wu-framework 2.1.2 → 2.6.0

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 (72) hide show
  1. package/README.md +6 -1
  2. package/dist/adapters/alpine/index.d.ts +1 -1
  3. package/dist/adapters/angular/index.d.ts +1 -1
  4. package/dist/adapters/htmx/index.d.ts +1 -1
  5. package/dist/adapters/lit/index.d.ts +1 -1
  6. package/dist/adapters/lit/index.js +2 -2
  7. package/dist/adapters/lit/index.js.map +1 -1
  8. package/dist/adapters/preact/index.d.ts +1 -1
  9. package/dist/adapters/preact/index.js +1 -1
  10. package/dist/adapters/preact/index.js.map +1 -1
  11. package/dist/adapters/qwik/index.d.ts +3 -10
  12. package/dist/adapters/qwik/index.js +1 -1
  13. package/dist/adapters/qwik/index.js.map +1 -1
  14. package/dist/adapters/react/index.js +1 -1
  15. package/dist/adapters/react/index.js.map +1 -1
  16. package/dist/adapters/shared.d.ts +44 -0
  17. package/dist/adapters/shared.js +1 -1
  18. package/dist/adapters/shared.js.map +1 -1
  19. package/dist/adapters/solid/index.d.ts +1 -1
  20. package/dist/adapters/solid/index.js +1 -1
  21. package/dist/adapters/solid/index.js.map +1 -1
  22. package/dist/adapters/stencil/index.d.ts +1 -1
  23. package/dist/adapters/stimulus/index.d.ts +1 -1
  24. package/dist/adapters/svelte/index.d.ts +1 -1
  25. package/dist/adapters/svelte/index.js +1 -1
  26. package/dist/adapters/svelte/index.js.map +1 -1
  27. package/dist/adapters/vanilla/index.d.ts +1 -1
  28. package/dist/adapters/vanilla/index.js +1 -1
  29. package/dist/adapters/vanilla/index.js.map +1 -1
  30. package/dist/adapters/vue/index.js +1 -1
  31. package/dist/adapters/vue/index.js.map +1 -1
  32. package/dist/ai/wu-ai.js +1 -1
  33. package/dist/ai/wu-ai.js.map +1 -1
  34. package/dist/core/wu-devtools.js +2 -0
  35. package/dist/core/wu-devtools.js.map +1 -0
  36. package/dist/core/wu-html-parser.js +1 -1
  37. package/dist/core/wu-html-parser.js.map +1 -1
  38. package/dist/core/wu-iframe-sandbox.js +1 -1
  39. package/dist/core/wu-iframe-sandbox.js.map +1 -1
  40. package/dist/core/wu-loader.js +1 -1
  41. package/dist/core/wu-loader.js.map +1 -1
  42. package/dist/core/wu-logger.js +2 -0
  43. package/dist/core/wu-logger.js.map +1 -0
  44. package/dist/core/wu-mcp-bridge.js +1 -1
  45. package/dist/core/wu-mcp-bridge.js.map +1 -1
  46. package/dist/core/wu-script-executor.js +1 -1
  47. package/dist/core/wu-script-executor.js.map +1 -1
  48. package/dist/core/wu-store-sync.js +2 -0
  49. package/dist/core/wu-store-sync.js.map +1 -0
  50. package/dist/core/wu-timeline.js +2 -0
  51. package/dist/core/wu-timeline.js.map +1 -0
  52. package/dist/index.d.cts +759 -0
  53. package/dist/index.d.ts +315 -1
  54. package/dist/wu-ai-browser-primitives-CaUCk1Xl.js +2 -0
  55. package/dist/wu-ai-browser-primitives-CaUCk1Xl.js.map +1 -0
  56. package/dist/wu-framework.cjs +3 -0
  57. package/dist/wu-framework.cjs.map +1 -0
  58. package/dist/wu-framework.dev.js +1296 -275
  59. package/dist/wu-framework.dev.js.map +1 -1
  60. package/dist/wu-framework.esm.js +2 -2
  61. package/dist/wu-framework.esm.js.map +1 -1
  62. package/dist/wu-framework.umd.js +2 -2
  63. package/dist/wu-framework.umd.js.map +1 -1
  64. package/integrations/astro/WuApp.astro +16 -11
  65. package/integrations/astro/WuShell.astro +11 -3
  66. package/package.json +14 -6
  67. package/dist/wu-ai-browser-primitives-BDKXJlwc.js +0 -2
  68. package/dist/wu-ai-browser-primitives-BDKXJlwc.js.map +0 -1
  69. package/dist/wu-framework.cjs.js +0 -3
  70. package/dist/wu-framework.cjs.js.map +0 -1
  71. package/dist/wu-logger-fJfUHBGA.js +0 -2
  72. package/dist/wu-logger-fJfUHBGA.js.map +0 -1
@@ -1,150 +1,8 @@
1
- /*! wu-framework v2.1.1 | MIT License */
1
+ /*! wu-framework v2.6.0 | MIT License */
2
+ import { logger } from './core/wu-logger.js';
3
+ export { enableAllLogs, silenceAllLogs } from './core/wu-logger.js';
2
4
  export { WuLoader } from './core/wu-loader.js';
3
5
 
4
- /**
5
- * 📝 WU-LOGGER: Sistema de logging inteligente para entornos
6
- * Controla los logs automáticamente según el entorno
7
- */
8
-
9
- class WuLogger {
10
- constructor() {
11
- // Detectar entorno automáticamente
12
- this.isDevelopment = this.detectEnvironment();
13
- // En desarrollo: warn (menos ruido), en producción: error
14
- this.logLevel = this.isDevelopment ? 'warn' : 'error';
15
-
16
- this.levels = {
17
- debug: 0,
18
- info: 1,
19
- warn: 2,
20
- error: 3,
21
- silent: 4
22
- };
23
- }
24
-
25
- /**
26
- * Detectar si estamos en desarrollo
27
- */
28
- detectEnvironment() {
29
- // 1. Explicit flag takes priority
30
- if (typeof window !== 'undefined' && window.WU_DEBUG === true) return true;
31
- if (typeof window !== 'undefined' && window.WU_DEBUG === false) return false;
32
-
33
- // 2. NODE_ENV check (works in bundlers and Node)
34
- if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return false;
35
- if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') return true;
36
-
37
- // 3. Browser heuristics (only if window exists)
38
- if (typeof window !== 'undefined' && window.location) {
39
- const hostname = window.location.hostname;
40
- if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]') return true;
41
-
42
- // URL param override
43
- try {
44
- if (new URLSearchParams(window.location.search).has('wu-debug')) return true;
45
- } catch {}
46
- }
47
-
48
- // 4. Default: assume production
49
- return false;
50
- }
51
-
52
- /**
53
- * Configurar nivel de logging
54
- */
55
- setLevel(level) {
56
- this.logLevel = level;
57
- return this;
58
- }
59
-
60
- /**
61
- * Habilitar/deshabilitar development mode
62
- */
63
- setDevelopment(isDev) {
64
- this.isDevelopment = isDev;
65
- this.logLevel = isDev ? 'debug' : 'error';
66
- return this;
67
- }
68
-
69
- /**
70
- * Verificar si debemos mostrar el log
71
- */
72
- shouldLog(level) {
73
- return this.levels[level] >= this.levels[this.logLevel];
74
- }
75
-
76
- /**
77
- * Logging methods
78
- */
79
- debug(...args) {
80
- if (this.shouldLog('debug')) {
81
- console.log(...args);
82
- }
83
- }
84
-
85
- info(...args) {
86
- if (this.shouldLog('info')) {
87
- console.info(...args);
88
- }
89
- }
90
-
91
- warn(...args) {
92
- if (this.shouldLog('warn')) {
93
- console.warn(...args);
94
- }
95
- }
96
-
97
- error(...args) {
98
- if (this.shouldLog('error')) {
99
- console.error(...args);
100
- }
101
- }
102
-
103
- /**
104
- * Logging con contexto Wu
105
- */
106
- wu(level, ...args) {
107
- if (this.shouldLog(level)) {
108
- const method = level === 'debug' ? 'log' : level;
109
- console[method]('[Wu]', ...args);
110
- }
111
- }
112
-
113
- /**
114
- * Helper methods específicos para Wu
115
- */
116
- wuDebug(...args) { this.wu('debug', ...args); }
117
- wuInfo(...args) { this.wu('info', ...args); }
118
- wuWarn(...args) { this.wu('warn', ...args); }
119
- wuError(...args) { this.wu('error', ...args); }
120
- }
121
-
122
- // Singleton instance
123
- const logger = new WuLogger();
124
-
125
- /**
126
- * 🔇 Silenciar todos los logs de Wu Framework
127
- * Útil en producción para eliminar todo el ruido
128
- */
129
- function silenceAllLogs() {
130
- logger.setLevel('silent');
131
- }
132
-
133
- /**
134
- * 🔊 Restaurar logs (nivel debug)
135
- */
136
- function enableAllLogs() {
137
- logger.setLevel('debug');
138
- }
139
-
140
- var wuLogger = /*#__PURE__*/Object.freeze({
141
- __proto__: null,
142
- WuLogger: WuLogger,
143
- enableAllLogs: enableAllLogs,
144
- logger: logger,
145
- silenceAllLogs: silenceAllLogs
146
- });
147
-
148
6
  /**
149
7
  * 🎨 WU-STYLE-BRIDGE: SHADOW DOM STYLE SHARING SYSTEM
150
8
  *
@@ -509,12 +367,20 @@ class WuStyleBridge {
509
367
  injectInlineStyle(shadowRoot, style) {
510
368
  // Verificar si ya existe
511
369
  const viteId = style.viteId;
370
+ const contentHash = viteId ? null : this._hashStyleContent(style.content);
512
371
  if (viteId) {
513
372
  const existing = shadowRoot.querySelector(`style[data-wu-vite-id="${viteId}"]`);
514
373
  if (existing) {
515
374
  logger.debug(`[WuStyleBridge] ⏭️ Inline style already exists: ${viteId}`);
516
375
  return;
517
376
  }
377
+ } else {
378
+ // Sin viteId (ej. CSS-in-JS runtime): dedup por hash de contenido
379
+ const existing = shadowRoot.querySelector(`style[data-wu-content-hash="${contentHash}"]`);
380
+ if (existing) {
381
+ logger.debug(`[WuStyleBridge] ⏭️ Inline style already exists (content hash: ${contentHash})`);
382
+ return;
383
+ }
518
384
  }
519
385
 
520
386
  // Crear nuevo style tag
@@ -524,6 +390,8 @@ class WuStyleBridge {
524
390
  styleTag.setAttribute('data-wu-library', style.library || 'unknown');
525
391
  if (viteId) {
526
392
  styleTag.setAttribute('data-wu-vite-id', viteId);
393
+ } else {
394
+ styleTag.setAttribute('data-wu-content-hash', contentHash);
527
395
  }
528
396
 
529
397
  // Insertar al principio del shadow root
@@ -532,6 +400,19 @@ class WuStyleBridge {
532
400
  logger.debug(`[WuStyleBridge] 📝 Injected inline style: ${style.library || viteId}`);
533
401
  }
534
402
 
403
+ /**
404
+ * #️⃣ HASH DE CONTENIDO: Hash rápido (djb2) para dedup de estilos inline sin viteId
405
+ * @param {string} content
406
+ * @returns {string}
407
+ */
408
+ _hashStyleContent(content) {
409
+ let hash = 5381;
410
+ for (let i = 0; i < content.length; i++) {
411
+ hash = ((hash << 5) + hash + content.charCodeAt(i)) | 0;
412
+ }
413
+ return `${content.length}_${(hash >>> 0).toString(36)}`;
414
+ }
415
+
535
416
  /**
536
417
  * 📋 INYECTAR ADOPTED STYLESHEET: Comparte stylesheet constructable
537
418
  * @param {ShadowRoot} shadowRoot
@@ -2524,6 +2405,10 @@ class WuStore {
2524
2405
  // Pattern listeners for wildcards
2525
2406
  this.patternListeners = new Map();
2526
2407
 
2408
+ // Low-level write taps (WuTimeline journaling). Lazily allocated — null
2409
+ // until something taps in, so the common path pays zero overhead.
2410
+ this._taps = null;
2411
+
2527
2412
  // Performance metrics
2528
2413
  this.metrics = {
2529
2414
  reads: 0,
@@ -2594,6 +2479,15 @@ class WuStore {
2594
2479
  // Update state synchronously
2595
2480
  this.updateState(path, value);
2596
2481
 
2482
+ // Synchronous low-level taps (journaling). Fired with the real sequence
2483
+ // number so WuTimeline's entries align with the ring-buffer order.
2484
+ if (this._taps && this._taps.size) {
2485
+ for (const tap of this._taps) {
2486
+ try { tap(sequence, path, value); }
2487
+ catch (error) { console.error('[WuStore] Tap error:', error); }
2488
+ }
2489
+ }
2490
+
2597
2491
  // Schedule async notifications (non-blocking)
2598
2492
  queueMicrotask(() => {
2599
2493
  this.notify(path, value);
@@ -2603,6 +2497,40 @@ class WuStore {
2603
2497
  return sequence;
2604
2498
  }
2605
2499
 
2500
+ /**
2501
+ * Register a low-level write tap. Unlike on(), taps fire synchronously
2502
+ * inside set() for EVERY write with the ring-buffer sequence number — the
2503
+ * substrate WuTimeline journals from. Returns an unsubscribe function.
2504
+ * @param {(sequence: number, path: string, value: *) => void} fn
2505
+ * @returns {Function} unsubscribe
2506
+ */
2507
+ tap(fn) {
2508
+ if (typeof fn !== 'function') return () => {};
2509
+ if (!this._taps) this._taps = new Set();
2510
+ this._taps.add(fn);
2511
+ return () => { this._taps?.delete(fn); };
2512
+ }
2513
+
2514
+ /**
2515
+ * Replace the whole state and re-notify listeners — the seek primitive for
2516
+ * WuTimeline. Does NOT touch the ring buffer, cursor, or taps, so a rewind
2517
+ * is invisible to journaling (no feedback loop). Notification is synchronous
2518
+ * (an explicit seek should reflect immediately), guarded per-listener.
2519
+ *
2520
+ * @param {Object} newState - The state to install
2521
+ * @param {Object} [opts]
2522
+ * @param {string[]} [opts.notifyPaths] - Paths to re-notify (default: top-level keys)
2523
+ */
2524
+ hydrate(newState, { notifyPaths } = {}) {
2525
+ this.state = (newState && typeof newState === 'object') ? newState : {};
2526
+ const paths = notifyPaths || Object.keys(this.state);
2527
+ for (const path of paths) {
2528
+ const value = this.get(path);
2529
+ this.notify(path, value);
2530
+ this.notifyPatterns(path, value);
2531
+ }
2532
+ }
2533
+
2606
2534
  /**
2607
2535
  * Subscribe to state changes
2608
2536
  * @param {string} pattern - Path or pattern (supports * wildcard)
@@ -2791,6 +2719,87 @@ class WuStore {
2791
2719
  return regex.test(path);
2792
2720
  }
2793
2721
 
2722
+ /**
2723
+ * Start real-time collaborative sync of this store across replicas — tabs
2724
+ * (BroadcastChannel), workers, or clients (WebSocket) — conflict-free via a
2725
+ * per-path Last-Writer-Wins CRDT. Writes made after sync() propagate to
2726
+ * peers; remote writes merge in and notify subscribers. Lazy-loads the sync
2727
+ * chunk (zero cost until called). See ROADMAP.md (#2).
2728
+ *
2729
+ * @param {Object} [opts]
2730
+ * @param {'broadcast'|WebSocket|string|{send,onMessage,close}} [opts.transport='broadcast']
2731
+ * @param {string} [opts.room='default'] - Channel name for the broadcast transport
2732
+ * @returns {{ stop: Function, status: Function, ready: Function, connected: boolean, instance: any }}
2733
+ *
2734
+ * @example
2735
+ * wu.store.sync({ transport: 'broadcast', room: 'cart' }); // cross-tab
2736
+ * wu.store.sync({ transport: 'wss://sync.example/doc-42' }); // cross-client
2737
+ */
2738
+ sync(opts = {}) {
2739
+ // One active sync per store. A second call returns the existing handle
2740
+ // (different opts are ignored — stop() first to reconfigure).
2741
+ if (this._syncHandle && !this._syncHandle._stopped) {
2742
+ console.warn('[WuStore] sync() already active — returning the existing handle. stop() it first to reconfigure.');
2743
+ return this._syncHandle;
2744
+ }
2745
+
2746
+ const store = this;
2747
+ let inst = null;
2748
+ let loading = null;
2749
+ let stopped = false;
2750
+ let error = null;
2751
+
2752
+ const ensure = () => {
2753
+ if (inst) return Promise.resolve(inst);
2754
+ if (!loading) {
2755
+ loading = import('./core/wu-store-sync.js')
2756
+ .then(({ WuStoreSync }) => {
2757
+ if (stopped) return null; // stop() raced ahead of the load
2758
+ const s = new WuStoreSync(store);
2759
+ s.connect(opts); // may throw (resolveTransport)
2760
+ inst = s; // assign ONLY after connect succeeds
2761
+ return inst;
2762
+ })
2763
+ .catch((err) => {
2764
+ // A failed connect must not wedge the one-sync-per-store guard.
2765
+ // Clear it so a later sync() can retry; resolve to null (honest
2766
+ // no-op) rather than rejecting (no unhandled-rejection surprise).
2767
+ error = err;
2768
+ if (store._syncHandle === handle) store._syncHandle = null;
2769
+ console.warn('[WuStore] sync() failed to connect:', err?.message || err);
2770
+ return null;
2771
+ });
2772
+ }
2773
+ return loading;
2774
+ };
2775
+
2776
+ const handle = {
2777
+ stop() {
2778
+ stopped = true; this._stopped = true;
2779
+ if (store._syncHandle === handle) store._syncHandle = null;
2780
+ if (inst) inst.stop();
2781
+ },
2782
+ status() {
2783
+ if (inst) return inst.status();
2784
+ // Stable shape across the load boundary (mirrors the loaded status()).
2785
+ return {
2786
+ connected: false, site: null, lamport: 0, peers: 0, tracked: 0,
2787
+ sent: 0, received: 0, applied: 0, ignored: 0, dropped: 0,
2788
+ loading: !stopped && !error, stopped,
2789
+ error: error ? String(error.message || error) : null,
2790
+ };
2791
+ },
2792
+ ready() { return ensure(); }, // await to guarantee the connection
2793
+ get connected() { return inst ? inst.status().connected : false; },
2794
+ get instance() { return inst; },
2795
+ _stopped: false,
2796
+ };
2797
+
2798
+ this._syncHandle = handle;
2799
+ ensure();
2800
+ return handle;
2801
+ }
2802
+
2794
2803
  /**
2795
2804
  * Clear all state and listeners
2796
2805
  */
@@ -2900,6 +2909,13 @@ class WuApp {
2900
2909
  await this._wu.init({
2901
2910
  apps: [{ name: this.name, url: this.url }]
2902
2911
  });
2912
+ } else if (!this._wu.apps.get(this.name)?.manifest) {
2913
+ // wu ya estaba inicializado: cargar el manifest de esta app
2914
+ await this._wu.registerApp({
2915
+ name: this.name,
2916
+ url: this.url,
2917
+ keepAlive: this.keepAlive
2918
+ });
2903
2919
  }
2904
2920
 
2905
2921
  // Montar usando wu-framework core
@@ -2923,7 +2939,7 @@ class WuApp {
2923
2939
  }
2924
2940
 
2925
2941
  await this._wu.unmount(this.name, options);
2926
- this._mounted = !this._wu.isHidden(this.name);
2942
+ this._mounted = false;
2927
2943
 
2928
2944
  return this
2929
2945
  }
@@ -3261,9 +3277,20 @@ class WuCache {
3261
3277
  if (this.config.persistent) {
3262
3278
  const stored = this.getFromStorage(key);
3263
3279
  if (stored) {
3264
- // Restaurar a memoria
3265
- this.memoryCache.set(key, stored);
3266
- this.accessOrder.set(key, Date.now());
3280
+ // Verificar TTL
3281
+ if (this.isExpired(stored)) {
3282
+ this.delete(key);
3283
+ this.stats.misses++;
3284
+ return null;
3285
+ }
3286
+
3287
+ // Restaurar a memoria (evicción solo en memoria: una lectura no
3288
+ // debe borrar copias persistentes de otras claves)
3289
+ if (this.ensureSpace(stored.size || 0, { demoteOnly: true }) !== false) {
3290
+ this.memoryCache.set(key, stored);
3291
+ this.accessOrder.set(key, Date.now());
3292
+ this.stats.size += stored.size || 0;
3293
+ }
3267
3294
  this.stats.hits++;
3268
3295
  return stored.value;
3269
3296
  }
@@ -3303,7 +3330,11 @@ class WuCache {
3303
3330
  return false;
3304
3331
  }
3305
3332
 
3306
- // Guardar en memoria
3333
+ // Guardar en memoria (descontar la entrada previa si se sobreescribe)
3334
+ const existing = this.memoryCache.get(key);
3335
+ if (existing) {
3336
+ this.stats.size -= existing.size;
3337
+ }
3307
3338
  this.memoryCache.set(key, entry);
3308
3339
  this.accessOrder.set(key, Date.now());
3309
3340
 
@@ -3390,7 +3421,7 @@ class WuCache {
3390
3421
  * 🎯 ENSURE SPACE: Asegurar espacio en cache (LRU eviction)
3391
3422
  * @param {number} neededSize - Tamaño necesario
3392
3423
  */
3393
- ensureSpace(neededSize) {
3424
+ ensureSpace(neededSize, { demoteOnly = false } = {}) {
3394
3425
  const maxSizeBytes = this.config.maxSize * 1024 * 1024;
3395
3426
 
3396
3427
  // 🛡️ FIX: Validar que el item no sea más grande que el máximo permitido
@@ -3429,7 +3460,16 @@ class WuCache {
3429
3460
 
3430
3461
  if (oldestKey) {
3431
3462
  logger.debug(`[WuCache] 🗑️ Evicting LRU entry: ${oldestKey}`);
3432
- this.delete(oldestKey);
3463
+ if (demoteOnly) {
3464
+ // Memory-only eviction: keep the persistent copy so the evicted
3465
+ // entry stays restorable (a read must never destroy other keys)
3466
+ const evicted = this.memoryCache.get(oldestKey);
3467
+ if (evicted) this.stats.size -= evicted.size;
3468
+ this.memoryCache.delete(oldestKey);
3469
+ this.accessOrder.delete(oldestKey);
3470
+ } else {
3471
+ this.delete(oldestKey);
3472
+ }
3433
3473
  this.stats.evictions++;
3434
3474
  } else {
3435
3475
  break;
@@ -3634,6 +3674,12 @@ class WuEventBus {
3634
3674
  this.listeners = new Map();
3635
3675
  this.history = [];
3636
3676
 
3677
+ // Low-level emit taps (WuTimeline journaling). Lazily allocated — null
3678
+ // until something taps in. Unlike on('*'), taps fire after history is
3679
+ // recorded and bypass wildcard-regex matching, so journaling is cheap and
3680
+ // never affected by an app's authorization scope.
3681
+ this._taps = null;
3682
+
3637
3683
  // Cache of compiled wildcard patterns. Avoids `new RegExp()` per emit per listener.
3638
3684
  this._wildcardCache = new Map();
3639
3685
 
@@ -3857,11 +3903,23 @@ class WuEventBus {
3857
3903
  verified: this.authorizedApps.has(appName)
3858
3904
  };
3859
3905
 
3860
- // Agregar a historial
3861
- if (this.config.enableReplay) {
3906
+ // Agregar a historial. `options.history === false` skips it — for
3907
+ // internal firehose events (e.g. WuTimeline's timeline:* lifecycle) that
3908
+ // would otherwise flood the bounded history and evict real app events from
3909
+ // it and from wu.inspect().events.recent. Listeners still receive them.
3910
+ if (this.config.enableReplay && options.history !== false) {
3862
3911
  this.addToHistory(event);
3863
3912
  }
3864
3913
 
3914
+ // Low-level taps (journaling) fire before listener dispatch, so a throwing
3915
+ // listener can't stop an event from being recorded.
3916
+ if (this._taps && this._taps.size) {
3917
+ for (const tap of this._taps) {
3918
+ try { tap(event); }
3919
+ catch (error) { console.error('[WuEventBus] Tap error:', error); }
3920
+ }
3921
+ }
3922
+
3865
3923
  // Log si está habilitado
3866
3924
  if (this.config.logEvents) {
3867
3925
  logger.debug(`[WuEventBus] 📢 ${eventName}`, data);
@@ -3888,6 +3946,21 @@ class WuEventBus {
3888
3946
  return true;
3889
3947
  }
3890
3948
 
3949
+ /**
3950
+ * 📼 TAP: Register a low-level emit tap. Fires for EVERY emitted event
3951
+ * (after history, before listeners), receiving the full event object.
3952
+ * Bypasses wildcard matching and authorization — it's a framework-internal
3953
+ * firehose for WuTimeline journaling. Returns an unsubscribe function.
3954
+ * @param {(event: WuEvent) => void} fn
3955
+ * @returns {Function} unsubscribe
3956
+ */
3957
+ tap(fn) {
3958
+ if (typeof fn !== 'function') return () => {};
3959
+ if (!this._taps) this._taps = new Set();
3960
+ this._taps.add(fn);
3961
+ return () => { this._taps?.delete(fn); };
3962
+ }
3963
+
3891
3964
  /**
3892
3965
  * 👂 ON: Suscribirse a evento
3893
3966
  */
@@ -3921,8 +3994,8 @@ class WuEventBus {
3921
3994
  */
3922
3995
  once(eventName, callback) {
3923
3996
  const wrappedCallback = (event) => {
3924
- callback(event);
3925
3997
  this.off(eventName, wrappedCallback);
3998
+ callback(event);
3926
3999
  };
3927
4000
  return this.on(eventName, wrappedCallback);
3928
4001
  }
@@ -4222,6 +4295,11 @@ class WuPerformance {
4222
4295
  const appMetrics = this.metrics.get(measurement.appName);
4223
4296
  appMetrics.measurements.push(measurement);
4224
4297
 
4298
+ // Mantener tamaño máximo por app
4299
+ if (appMetrics.measurements.length > this.config.maxMeasurements) {
4300
+ appMetrics.measurements.shift();
4301
+ }
4302
+
4225
4303
  // Calcular estadísticas
4226
4304
  this.calculateStats(measurement.appName);
4227
4305
  }
@@ -4699,11 +4777,13 @@ class WuPluginSystem {
4699
4777
  }
4700
4778
 
4701
4779
  // Registrar hooks del plugin con protección
4780
+ const registeredHooks = [];
4702
4781
  this.availableHooks.forEach(hookName => {
4703
4782
  if (typeof plugin[hookName] === 'function') {
4704
4783
  // Wrap el hook con timeout y try-catch
4705
4784
  const wrappedHook = this._wrapHook(plugin[hookName].bind(plugin), plugin.name, hookName);
4706
4785
  this.registerHook(hookName, wrappedHook);
4786
+ registeredHooks.push({ hookName, callback: wrappedHook });
4707
4787
  }
4708
4788
  });
4709
4789
 
@@ -4713,6 +4793,7 @@ class WuPluginSystem {
4713
4793
  options,
4714
4794
  permissions,
4715
4795
  sandboxedApi,
4796
+ registeredHooks,
4716
4797
  installedAt: Date.now()
4717
4798
  });
4718
4799
 
@@ -4759,7 +4840,9 @@ class WuPluginSystem {
4759
4840
  * 🎯 CALL HOOK
4760
4841
  */
4761
4842
  async callHook(hookName, context) {
4762
- const callbacks = this.hooks.get(hookName) || [];
4843
+ // Snapshot: uninstall() splices these arrays and would shift the
4844
+ // iterator past the next plugin's hook mid-dispatch
4845
+ const callbacks = [...(this.hooks.get(hookName) || [])];
4763
4846
 
4764
4847
  for (const callback of callbacks) {
4765
4848
  try {
@@ -4785,7 +4868,7 @@ class WuPluginSystem {
4785
4868
  return;
4786
4869
  }
4787
4870
 
4788
- const { plugin, sandboxedApi } = pluginData;
4871
+ const { plugin, sandboxedApi, registeredHooks } = pluginData;
4789
4872
 
4790
4873
  if (plugin.uninstall) {
4791
4874
  try {
@@ -4795,6 +4878,17 @@ class WuPluginSystem {
4795
4878
  }
4796
4879
  }
4797
4880
 
4881
+ // Remover los hooks registrados por el plugin
4882
+ if (registeredHooks) {
4883
+ for (const { hookName, callback } of registeredHooks) {
4884
+ const callbacks = this.hooks.get(hookName);
4885
+ if (callbacks) {
4886
+ const index = callbacks.indexOf(callback);
4887
+ if (index > -1) callbacks.splice(index, 1);
4888
+ }
4889
+ }
4890
+ }
4891
+
4798
4892
  this.plugins.delete(pluginName);
4799
4893
  logger.debug(`[WuPlugin] ✅ Plugin "${pluginName}" uninstalled`);
4800
4894
  }
@@ -5018,6 +5112,10 @@ class WuLoadingStrategy {
5018
5112
  async preload(apps) {
5019
5113
  const toPreload = apps.filter(app => {
5020
5114
  const strategy = this.strategies.get(app.strategy || 'lazy');
5115
+ if (!strategy) {
5116
+ logger.warn(`[WuStrategies] Strategy "${app.strategy}" not found, using lazy`);
5117
+ return false;
5118
+ }
5021
5119
  return strategy.shouldPreload;
5022
5120
  });
5023
5121
 
@@ -5561,6 +5659,8 @@ class WuLifecycleHooks {
5561
5659
  'afterLoad', // Después de cargar
5562
5660
  'beforeMount', // Antes de montar
5563
5661
  'afterMount', // Después de montar
5662
+ 'beforeUpdate', // Antes de empujar props vivas (wu.update)
5663
+ 'afterUpdate', // Después de empujar props vivas
5564
5664
  'beforeUnmount', // Antes de desmontar
5565
5665
  'afterUnmount', // Después de desmontar
5566
5666
  'beforeDestroy', // Antes de destruir framework
@@ -5661,10 +5761,9 @@ class WuLifecycleHooks {
5661
5761
 
5662
5762
  const hook = hooks[index];
5663
5763
  const startTime = Date.now();
5764
+ let nextCalled = false;
5664
5765
 
5665
5766
  try {
5666
- let nextCalled = false;
5667
-
5668
5767
  // Función next
5669
5768
  const next = async (modifiedContext) => {
5670
5769
  nextCalled = true;
@@ -5694,8 +5793,16 @@ class WuLifecycleHooks {
5694
5793
  } catch (error) {
5695
5794
  console.error(`[WuHooks] Error in hook "${hook.name}":`, error);
5696
5795
 
5697
- // Si hay error, pasar al siguiente hook
5698
- return await executeChain(index + 1);
5796
+ // Si hay error antes de llamar next(), pasar al siguiente hook.
5797
+ // Si next() ya se llamó, los hooks siguientes ya se ejecutaron.
5798
+ if (!nextCalled) {
5799
+ return await executeChain(index + 1);
5800
+ }
5801
+ }
5802
+
5803
+ // Propagar cancelación de hooks posteriores en la cadena
5804
+ if (cancelled) {
5805
+ return { cancelled: true };
5699
5806
  }
5700
5807
 
5701
5808
  return currentContext;
@@ -5996,15 +6103,16 @@ class WuPrefetch {
5996
6103
  const urls = await this._resolveAppUrls(appNames);
5997
6104
  if (urls.length === 0) return;
5998
6105
 
5999
- // Mark all as prefetched by name (prevents duplicate resolution)
6000
- urls.forEach(({ name }) => this.prefetched.add(name));
6001
-
6002
6106
  // Strategy 1: Speculation Rules API (Chrome 121+)
6107
+ // (_addSpeculationRules marks the names as prefetched itself)
6003
6108
  if (this.supportsSpeculationRules) {
6004
6109
  this._addSpeculationRules(urls, options.eagerness || 'moderate');
6005
6110
  return;
6006
6111
  }
6007
6112
 
6113
+ // Mark all as prefetched by name (prevents duplicate resolution)
6114
+ urls.forEach(({ name }) => this.prefetched.add(name));
6115
+
6008
6116
  // Strategy 2: <link rel="modulepreload"> for ES modules
6009
6117
  if (this.supportsModulePreload) {
6010
6118
  urls.forEach(({ url }) => this._injectModulePreload(url));
@@ -6819,6 +6927,413 @@ class WuOverrides {
6819
6927
  }
6820
6928
  }
6821
6929
 
6930
+ /**
6931
+ * WU-SEMVER: minimal semantic-version range matching (zero-dep).
6932
+ *
6933
+ * Enough for capability-contract negotiation (`wu.consume('cart', '^2.0')`),
6934
+ * not a full node-semver. Supports:
6935
+ * - exact: 1.2.3
6936
+ * - X-ranges: 1, 1.2, 1.x, *, "" (match anything)
6937
+ * - caret: ^1.2.3 (compatible within the leftmost non-zero)
6938
+ * - tilde: ~1.2.3 (patch-level within the minor)
6939
+ * - comparators: >= > <= < =
6940
+ * - AND: ">=1.0.0 <2.0.0" (space-separated)
6941
+ * - OR: "^1.0 || ^2.0"
6942
+ * Prerelease / build metadata is stripped (compared on major.minor.patch).
6943
+ */
6944
+
6945
+ function num(x) {
6946
+ const n = parseInt(x, 10);
6947
+ return Number.isNaN(n) ? 0 : n;
6948
+ }
6949
+
6950
+ /** Parse a version string to a [major, minor, patch] tuple. */
6951
+ function parseVersion(v) {
6952
+ const core = String(v).trim().replace(/^[v=\s]+/, '').split('+')[0].split('-')[0];
6953
+ const parts = core.split('.');
6954
+ return [num(parts[0]), num(parts[1]), num(parts[2])];
6955
+ }
6956
+
6957
+ /** Compare two versions (string or parsed tuple): -1 | 0 | 1. */
6958
+ function compareVersions(a, b) {
6959
+ const pa = Array.isArray(a) ? a : parseVersion(a);
6960
+ const pb = Array.isArray(b) ? b : parseVersion(b);
6961
+ for (let i = 0; i < 3; i++) {
6962
+ if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
6963
+ }
6964
+ return 0;
6965
+ }
6966
+
6967
+ function satisfiesComparator(version, raw) {
6968
+ const comp = raw.trim();
6969
+ if (comp === '' || comp === '*' || comp === 'x' || comp === 'X') return true;
6970
+
6971
+ // caret (^) / tilde (~)
6972
+ if (comp[0] === '^' || comp[0] === '~') {
6973
+ const caret = comp[0] === '^';
6974
+ const spec = comp.slice(1).trim();
6975
+ const base = parseVersion(spec);
6976
+ if (compareVersions(version, base) < 0) return false;
6977
+ // Count only CONCRETE leading components (x/X/* are unspecified), so the
6978
+ // upper bound for partial / zero ranges matches npm's X-range desugaring.
6979
+ const specifiedCount = spec
6980
+ .replace(/^[v=]+/, '').split('+')[0].split('-')[0]
6981
+ .split('.').filter((p) => p !== '' && p !== 'x' && p !== 'X' && p !== '*').length;
6982
+ let upper;
6983
+ if (caret) {
6984
+ // ^1.2.3 → <2; ^0.2.3 → <0.3; ^0.0.3 → <0.0.4; but ^0 → <1, ^0.0 → <0.1
6985
+ // (the leftmost non-zero rule needs how many components were written).
6986
+ if (base[0] > 0) upper = [base[0] + 1, 0, 0];
6987
+ else if (specifiedCount < 2) upper = [1, 0, 0];
6988
+ else if (base[1] > 0) upper = [0, base[1] + 1, 0];
6989
+ else if (specifiedCount < 3) upper = [0, 1, 0];
6990
+ else upper = [0, 0, base[2] + 1];
6991
+ } else {
6992
+ // ~1.2.3 / ~1.2 → <1.3.0 ; ~1 → <2.0.0
6993
+ upper = specifiedCount >= 2 ? [base[0], base[1] + 1, 0] : [base[0] + 1, 0, 0];
6994
+ }
6995
+ return compareVersions(parseVersion(version), upper) < 0;
6996
+ }
6997
+
6998
+ // explicit comparator
6999
+ const m = comp.match(/^(>=|<=|>|<|=)\s*(.+)$/);
7000
+ if (m) {
7001
+ const c = compareVersions(version, parseVersion(m[2]));
7002
+ switch (m[1]) {
7003
+ case '>': return c > 0;
7004
+ case '>=': return c >= 0;
7005
+ case '<': return c < 0;
7006
+ case '<=': return c <= 0;
7007
+ default: return c === 0; // '='
7008
+ }
7009
+ }
7010
+
7011
+ // bare version → X-range if partial / wildcarded, else exact
7012
+ const rawParts = comp.replace(/^[v=]+/, '').split('+')[0].split('-')[0].split('.');
7013
+ const wildcarded = rawParts.some((p) => p === 'x' || p === 'X' || p === '*');
7014
+ if (rawParts.length < 3 || wildcarded) {
7015
+ const vp = parseVersion(version);
7016
+ for (let i = 0; i < 3; i++) {
7017
+ const rp = rawParts[i];
7018
+ if (rp === undefined || rp === '' || rp === 'x' || rp === 'X' || rp === '*') continue;
7019
+ if (vp[i] !== num(rp)) return false;
7020
+ }
7021
+ return true;
7022
+ }
7023
+ return compareVersions(version, parseVersion(comp)) === 0;
7024
+ }
7025
+
7026
+ /**
7027
+ * Does `version` satisfy `range`?
7028
+ * @param {string} version - e.g. "2.3.0"
7029
+ * @param {string} range - e.g. "^2.0", "~1.2.3", ">=1.0.0 <2.0.0", "1.x || 2.x"
7030
+ * @returns {boolean}
7031
+ */
7032
+ function satisfies(version, range) {
7033
+ if (range === null || range === undefined || range === '' || range === '*') return true;
7034
+ return String(range).split('||').some((group) =>
7035
+ group.trim().split(/\s+/).filter(Boolean).every((comp) => satisfiesComparator(version, comp))
7036
+ );
7037
+ }
7038
+
7039
+ /**
7040
+ * WU-CONTRACTS: typed, versioned, runtime-verified capability contracts.
7041
+ *
7042
+ * Kills Module Federation's worst failure mode — silent shared-singleton
7043
+ * version skew that explodes at runtime as `undefined is not a function`.
7044
+ * Instead of sharing fragile singletons, independently-deployed micro-apps
7045
+ * declare what they OFFER and what they NEED, and the runtime negotiates and
7046
+ * verifies:
7047
+ *
7048
+ * // Provider (team Cart, deployed independently)
7049
+ * wu.provide('cart', { add(item){…}, get count(){…} }, { version: '2.3.0', app: 'cart' });
7050
+ *
7051
+ * // Consumer (team Header)
7052
+ * const cart = wu.consume('cart', '^2.0'); // honest failure if unsatisfied
7053
+ * cart.add(product);
7054
+ *
7055
+ * - Semver negotiation: `consume('cart','^2.0')` against the provider's declared
7056
+ * version; a mismatch fails LOUDLY ("cart@1.4 does not satisfy ^2.0") instead
7057
+ * of MF's silent breakage.
7058
+ * - Runtime shape verification: an impl that drifts from its declared interface
7059
+ * fails at provide() time, not at the consumer's call site.
7060
+ * - Lifecycle-aware: consume() returns a LIVE proxy that re-resolves on every
7061
+ * access, so a capability becoming unavailable (provider unmounts) yields a
7062
+ * clear "no provider for 'cart'" — never a stale reference. Capabilities are
7063
+ * auto-revoked when their providing app unmounts.
7064
+ * - Event-backed: `wu:capability:provided` / `wu:capability:revoked` let late
7065
+ * consumers `await wu.consume('cart','^2.0',{ wait:true })`.
7066
+ *
7067
+ * Eager (in the main bundle, like the store and event bus) — provide/consume
7068
+ * must work synchronously the moment an app mounts.
7069
+ */
7070
+
7071
+
7072
+ class WuContracts {
7073
+ /**
7074
+ * @param {object} core - The WuCore instance (for the event bus + lifecycle).
7075
+ */
7076
+ constructor(core) {
7077
+ this.core = core;
7078
+ this._providers = new Map(); // name -> { impl, version, app, shape, providedAt }
7079
+ this._waiters = new Map(); // name -> Set<{ range, resolve, reject, timer }>
7080
+ // NOTE: auto-revoke on app unmount is driven by WuCore._executeUnmount
7081
+ // calling revokeByApp() with the FRAMEWORK-VERIFIED app name — NOT by
7082
+ // listening to the app:unmounted bus event. The bus treats any 'app:'
7083
+ // event as system-trusted (no token), so a malicious micro-app could forge
7084
+ // app:unmounted with a rival's appName to revoke its capability. Driving it
7085
+ // from the core's own unmount path closes that spoofing vector.
7086
+ }
7087
+
7088
+ /**
7089
+ * Register a capability implementation.
7090
+ * @param {string} name
7091
+ * @param {object|Function} impl
7092
+ * @param {object} [opts]
7093
+ * @param {string} [opts.version='0.0.0'] - Semver the impl satisfies
7094
+ * @param {string[]|Object<string,string>} [opts.shape] - Required keys, or key→typeof
7095
+ * @param {string} [opts.app] - Providing app; the capability auto-revokes on its unmount
7096
+ * @returns {{ revoke: Function }}
7097
+ */
7098
+ provide(name, impl, opts = {}) {
7099
+ if (!name || typeof name !== 'string') {
7100
+ throw new Error('[WuContracts] provide() requires a capability name (string).');
7101
+ }
7102
+ if (impl === null || impl === undefined || (typeof impl !== 'object' && typeof impl !== 'function')) {
7103
+ throw new Error(`[WuContracts] provide('${name}') requires an implementation object or function.`);
7104
+ }
7105
+ const version = opts.version || '0.0.0';
7106
+ if (opts.shape) this._verifyShape(name, impl, opts.shape, version);
7107
+
7108
+ const prev = this._providers.get(name);
7109
+ if (prev && prev.version !== version) {
7110
+ logger.wuDebug?.(`[WuContracts] '${name}' re-provided: ${prev.version} → ${version}`);
7111
+ }
7112
+
7113
+ const record = { impl, version, app: opts.app || null, shape: opts.shape || null, providedAt: Date.now() };
7114
+ this._providers.set(name, record);
7115
+
7116
+ this._emit('wu:capability:provided', { name, version, app: record.app });
7117
+ this._resolveWaiters(name, record);
7118
+ logger.wuDebug?.(`[WuContracts] provided '${name}@${version}'`);
7119
+
7120
+ // Identity-scoped revoke: only revoke if THIS record is still the current
7121
+ // owner of the name. Prevents a stale handle (after a same-name re-provide
7122
+ // by another app) from killing the new owner's live capability.
7123
+ return {
7124
+ revoke: () => (this._providers.get(name) === record ? this.revoke(name) : false),
7125
+ };
7126
+ }
7127
+
7128
+ /**
7129
+ * Consume a capability. Returns a LIVE proxy (re-resolves on every access) —
7130
+ * never throws at consume() time, so you can consume before the provider
7131
+ * mounts; the first ACCESS throws a clear error if unsatisfied.
7132
+ *
7133
+ * With `{ wait: true }`, returns a Promise that resolves once a satisfying
7134
+ * provider appears (optionally bounded by `timeout` ms).
7135
+ *
7136
+ * @param {string} name
7137
+ * @param {string} [range='*'] - Semver range the provider must satisfy
7138
+ * @param {object} [opts]
7139
+ * @param {boolean} [opts.wait=false]
7140
+ * @param {number} [opts.timeout] - ms; rejects the wait promise if exceeded
7141
+ * @returns {Proxy|Promise<Proxy>}
7142
+ */
7143
+ consume(name, range = '*', opts = {}) {
7144
+ if (!opts.wait) return this._proxy(name, range);
7145
+
7146
+ const cur = this._providers.get(name);
7147
+ if (cur && satisfies(cur.version, range)) return Promise.resolve(this._proxy(name, range));
7148
+
7149
+ return new Promise((resolve, reject) => {
7150
+ const waiter = {
7151
+ range,
7152
+ resolve: () => resolve(this._proxy(name, range)),
7153
+ reject,
7154
+ timer: null,
7155
+ };
7156
+ if (opts.timeout) {
7157
+ waiter.timer = setTimeout(() => {
7158
+ this._removeWaiter(name, waiter);
7159
+ reject(new Error(`[WuContracts] timed out after ${opts.timeout}ms waiting for '${name}' satisfying '${range}'.`));
7160
+ }, opts.timeout);
7161
+ }
7162
+ this._addWaiter(name, waiter);
7163
+ });
7164
+ }
7165
+
7166
+ /** Is a satisfying provider currently registered? (never throws) */
7167
+ has(name, range = '*') {
7168
+ const rec = this._providers.get(name);
7169
+ return !!rec && satisfies(rec.version, range);
7170
+ }
7171
+
7172
+ /** Revoke a capability (e.g. on teardown). */
7173
+ revoke(name) {
7174
+ if (this._providers.delete(name)) {
7175
+ this._emit('wu:capability:revoked', { name });
7176
+ logger.wuDebug?.(`[WuContracts] revoked '${name}'`);
7177
+ return true;
7178
+ }
7179
+ return false;
7180
+ }
7181
+
7182
+ /**
7183
+ * Revoke every capability provided by `app`. Called by WuCore on a real
7184
+ * unmount with the framework-verified app name (never from a bus event).
7185
+ * @param {string} app
7186
+ */
7187
+ revokeByApp(app) {
7188
+ if (!app) return;
7189
+ for (const [name, rec] of [...this._providers]) {
7190
+ if (rec.app === app) this.revoke(name);
7191
+ }
7192
+ }
7193
+
7194
+ /** Snapshot of registered capabilities (for wu.inspect() / devtools). */
7195
+ list() {
7196
+ const out = [];
7197
+ for (const [name, rec] of this._providers) {
7198
+ out.push({ name, version: rec.version, app: rec.app });
7199
+ }
7200
+ return out;
7201
+ }
7202
+
7203
+ cleanup() {
7204
+ for (const set of this._waiters.values()) {
7205
+ for (const w of set) {
7206
+ if (w.timer) clearTimeout(w.timer);
7207
+ try { w.reject(new Error('[WuContracts] framework destroyed')); } catch { /* noop */ }
7208
+ }
7209
+ }
7210
+ this._waiters.clear();
7211
+ this._providers.clear();
7212
+ }
7213
+
7214
+ /* ─────────────────────────────── private ────────────────────────── */
7215
+
7216
+ _proxy(name, range) {
7217
+ const contracts = this;
7218
+ const resolve = () => {
7219
+ const rec = contracts._providers.get(name);
7220
+ if (!rec) {
7221
+ throw new Error(`[WuContracts] no provider for capability '${name}'. ` +
7222
+ `Did the providing app mount and call wu.provide('${name}', …)?`);
7223
+ }
7224
+ if (!satisfies(rec.version, range)) {
7225
+ throw new Error(`[WuContracts] capability '${name}@${rec.version}' does not satisfy '${range}'.`);
7226
+ }
7227
+ return rec;
7228
+ };
7229
+
7230
+ // Memoize bound methods so `proxy.fn === proxy.fn` (stable identity needed
7231
+ // for add/removeListener round-trips, Set/Map keys, React deps). Keyed by
7232
+ // the unbound source fn — a swapped provider yields a new fn → new binding,
7233
+ // so re-resolution stays live.
7234
+ const boundCache = new WeakMap();
7235
+ const bind = (impl, fn) => {
7236
+ let b = boundCache.get(fn);
7237
+ if (!b) { b = fn.bind(impl); boundCache.set(fn, b); }
7238
+ return b;
7239
+ };
7240
+
7241
+ return new Proxy(Object.create(null), {
7242
+ get(_, prop) {
7243
+ if (prop === '__wuCapability') return name;
7244
+ if (prop === '__wuRange') return range;
7245
+ if (prop === '__wuAvailable') return contracts.has(name, range);
7246
+ // The proxy is NEVER thenable. A capability method literally named
7247
+ // `then` is unreachable via the proxy (documented) — far safer than
7248
+ // silently assimilating into a Promise on await/Promise.resolve.
7249
+ if (prop === 'then') return undefined;
7250
+ // Don't throw on Symbol probes (iterators, toPrimitive, …).
7251
+ if (typeof prop === 'symbol') {
7252
+ if (!contracts.has(name, range)) return undefined;
7253
+ const rec = contracts._providers.get(name);
7254
+ const v = rec.impl[prop];
7255
+ return typeof v === 'function' ? bind(rec.impl, v) : v;
7256
+ }
7257
+ const rec = resolve();
7258
+ const val = rec.impl[prop];
7259
+ return typeof val === 'function' ? bind(rec.impl, val) : val;
7260
+ },
7261
+ set(_, prop) {
7262
+ // Consumed capabilities are READ-ONLY — a consumer writing through the
7263
+ // proxy would mutate the provider's shared impl (and could repoint its
7264
+ // prototype via __proto__). Honest no-op + warn.
7265
+ logger.wuWarn?.(`[WuContracts] ignored write to '${String(prop)}' on consumed capability '${name}': consumed capabilities are read-only.`);
7266
+ return true;
7267
+ },
7268
+ has(_, prop) {
7269
+ const rec = contracts._providers.get(name);
7270
+ return rec ? (prop in rec.impl) : false;
7271
+ },
7272
+ });
7273
+ }
7274
+
7275
+ _verifyShape(name, impl, shape, version) {
7276
+ const problems = [];
7277
+ if (Array.isArray(shape)) {
7278
+ for (const key of shape) if (!WuContracts._provides(impl, key)) problems.push(`${key} (missing)`);
7279
+ } else if (shape && typeof shape === 'object') {
7280
+ for (const [key, type] of Object.entries(shape)) {
7281
+ if (!WuContracts._provides(impl, key)) problems.push(`${key} (missing)`);
7282
+ else if (type && typeof impl[key] !== type) problems.push(`${key} (expected ${type}, got ${typeof impl[key]})`);
7283
+ }
7284
+ }
7285
+ if (problems.length) {
7286
+ throw new Error(`[WuContracts] provide('${name}@${version}') violates its declared shape — ${problems.join(', ')}.`);
7287
+ }
7288
+ }
7289
+
7290
+ /**
7291
+ * Is `key` GENUINELY provided by impl? Walks own props + the impl's own
7292
+ * prototype chain (so class-instance methods count) but stops before
7293
+ * Object/Function.prototype — otherwise inherited toString/valueOf/hasOwnProperty
7294
+ * would let a hollow `{}` satisfy any contract demanding those names.
7295
+ * @private
7296
+ */
7297
+ static _provides(impl, key) {
7298
+ let obj = impl;
7299
+ while (obj && obj !== Object.prototype && obj !== Function.prototype) {
7300
+ if (Object.prototype.hasOwnProperty.call(obj, key)) return true;
7301
+ obj = Object.getPrototypeOf(obj);
7302
+ }
7303
+ return false;
7304
+ }
7305
+
7306
+ _addWaiter(name, waiter) {
7307
+ if (!this._waiters.has(name)) this._waiters.set(name, new Set());
7308
+ this._waiters.get(name).add(waiter);
7309
+ }
7310
+
7311
+ _removeWaiter(name, waiter) {
7312
+ const set = this._waiters.get(name);
7313
+ if (set) { set.delete(waiter); if (!set.size) this._waiters.delete(name); }
7314
+ }
7315
+
7316
+ _resolveWaiters(name, rec) {
7317
+ const set = this._waiters.get(name);
7318
+ if (!set) return;
7319
+ for (const w of [...set]) {
7320
+ if (satisfies(rec.version, w.range)) {
7321
+ if (w.timer) clearTimeout(w.timer);
7322
+ set.delete(w);
7323
+ w.resolve();
7324
+ }
7325
+ }
7326
+ if (!set.size) this._waiters.delete(name);
7327
+ }
7328
+
7329
+ _emit(event, data) {
7330
+ try {
7331
+ // `wu:` events are system-trusted by the bus (no token needed).
7332
+ this.core.eventBus?.emit(event, data, { appName: 'wu-core' });
7333
+ } catch { /* bus may be torn down */ }
7334
+ }
7335
+ }
7336
+
6822
7337
  /**
6823
7338
  * WU-FRAMEWORK: UNIVERSAL MICROFRONTENDS
6824
7339
  * Motor principal agnostico - Funciona con cualquier framework
@@ -6834,7 +7349,8 @@ class WuCore {
6834
7349
  this.mounted = new Map(); // Apps montadas
6835
7350
  this.hidden = new Map(); // Keep-alive hidden apps
6836
7351
  this._pendingUnmounts = new Map(); // Deferred unmount timers (StrictMode compat)
6837
- this._mountingPromises = new Map(); // In-flight mount dedup
7352
+ this._mountingPromises = new Map(); // In-flight mount dedup: appName -> { promise, containerSelector }
7353
+ this._unmountingPromises = new Map(); // In-flight deferred teardowns: appName -> Promise
6838
7354
  this._defineWaiters = new Map(); // Promise waiters for wu.define(): appName -> { resolve, timer, promise }
6839
7355
  this._resolvedPaths = new Map(); // Cached module URL: cacheKey -> resolved path
6840
7356
  // Reference counting for mount/unmount. Survives multi-cycle StrictMode +
@@ -6856,6 +7372,9 @@ class WuCore {
6856
7372
  // Loader for wu.use() — same lazy pattern. Only instantiated when a
6857
7373
  // caller actually requests a shared component. See _ensureLoader().
6858
7374
  this.loader = null;
7375
+ // Time-travel recorder — lazy chunk, instantiated on first wu.timeline.*
7376
+ // access. See _ensureTimeline().
7377
+ this._timeline = null;
6859
7378
 
6860
7379
  // Sistemas esenciales
6861
7380
  // Cache: localStorage backing has a real-world ceiling of ~5MB per origin.
@@ -6873,10 +7392,19 @@ class WuCore {
6873
7392
  this.hooks = new WuLifecycleHooks(this);
6874
7393
  this.prefetcher = new WuPrefetch(this);
6875
7394
  this.overrides = new WuOverrides();
7395
+ // Capability contracts — typed/versioned provide/consume between apps.
7396
+ // Eager (like store/eventBus): provide/consume must work synchronously
7397
+ // the moment an app mounts. Needs eventBus, which is created above.
7398
+ this.contracts = new WuContracts(this);
6876
7399
 
6877
7400
  // Estado
6878
7401
  this.isInitialized = false;
6879
7402
 
7403
+ // RBAC: principal actual (quién usa el sistema). Las apps que declaran
7404
+ // `roles` (en su config o manifest) solo se montan si el principal está
7405
+ // autorizado. null = sin principal (las apps restringidas se niegan).
7406
+ this._principal = null;
7407
+
6880
7408
  logger.wuInfo('Wu Framework initialized - Universal Microfrontends');
6881
7409
  }
6882
7410
 
@@ -6972,6 +7500,63 @@ class WuCore {
6972
7500
  }
6973
7501
  }
6974
7502
 
7503
+ // ── RBAC: aislamiento rol↔módulo (declarativo, enforced en mount) ──────────
7504
+
7505
+ /**
7506
+ * Fija el principal actual (rol/permisos del usuario). Las apps que declaran
7507
+ * `roles` solo se montan si el principal está autorizado. Se refleja también
7508
+ * en wu.store('auth.principal') para que apps/DevTools lo lean.
7509
+ * @param {{role?: string, roles?: string[], permissions?: string[]}|null} principal
7510
+ * @returns {Object|null} el principal efectivo
7511
+ */
7512
+ setPrincipal(principal) {
7513
+ this._principal = principal || null;
7514
+ try { this.store.set('auth.principal', this._principal); } catch { /* store opcional */ }
7515
+ this.eventBus.emit('principal:changed', { principal: this._principal });
7516
+ return this._principal;
7517
+ }
7518
+
7519
+ /** @returns {Object|null} el principal actual */
7520
+ getPrincipal() {
7521
+ return this._principal;
7522
+ }
7523
+
7524
+ /** Roles permitidos para montar `appName` (de config o manifest), o null si es público. */
7525
+ _requiredRoles(appName) {
7526
+ const app = this.apps.get(appName);
7527
+ const manifest = this.manifests.get(appName);
7528
+ const roles = app?.roles ?? manifest?.wu?.roles ?? manifest?.roles ?? null;
7529
+ return Array.isArray(roles) && roles.length ? roles : null;
7530
+ }
7531
+
7532
+ /** Roles que porta el principal actual (role + roles[]). */
7533
+ _principalRoles() {
7534
+ const p = this._principal;
7535
+ if (!p) return [];
7536
+ const out = [];
7537
+ if (typeof p.role === 'string') out.push(p.role);
7538
+ if (Array.isArray(p.roles)) out.push(...p.roles);
7539
+ return out;
7540
+ }
7541
+
7542
+ /**
7543
+ * ¿Puede el principal actual montar/acceder a `appName`?
7544
+ * App pública (sin `roles`) → siempre true. App restringida → requiere un
7545
+ * principal cuyos roles intersecten, o un permiso `mount:*` / `mount:<app>`.
7546
+ * @param {string} appName
7547
+ * @returns {boolean}
7548
+ */
7549
+ can(appName) {
7550
+ const required = this._requiredRoles(appName);
7551
+ if (!required) return true; // módulo público
7552
+ const p = this._principal;
7553
+ if (!p) return false; // restringido y sin principal
7554
+ const perms = Array.isArray(p.permissions) ? p.permissions : [];
7555
+ if (perms.includes('mount:*') || perms.includes(`mount:${appName}`)) return true;
7556
+ const roles = this._principalRoles();
7557
+ return roles.some((r) => required.includes(r));
7558
+ }
7559
+
6975
7560
  /**
6976
7561
  * Definir lifecycle de una micro-app
6977
7562
  * @param {string} appName - Nombre de la app
@@ -7010,6 +7595,27 @@ class WuCore {
7010
7595
  * @param {string} containerSelector - Selector del contenedor
7011
7596
  */
7012
7597
  async mount(appName, containerSelector) {
7598
+ // ── RBAC: negar el montaje de módulos para los que el principal no está
7599
+ // autorizado. Se chequea ANTES del ref-counting → un deny es un early-throw
7600
+ // limpio, sin efectos. Emite `access:denied` (bus) y `wu:access:denied` (DOM)
7601
+ // para que el shell muestre un fallback. NOTA: es enforcement client-side —
7602
+ // mejora corrección/UX; la barrera dura es el server (no servir el bundle).
7603
+ if (!this.can(appName)) {
7604
+ const required = this._requiredRoles(appName) || [];
7605
+ const role = this._principal?.role ?? null;
7606
+ this.eventBus.emit('access:denied', { appName, role, required }, { appName });
7607
+ try {
7608
+ window.dispatchEvent(new CustomEvent('wu:access:denied', { detail: { appName, role, required } }));
7609
+ } catch { /* no-DOM env */ }
7610
+ const err = new Error(
7611
+ `[Wu] Access denied: el rol '${role ?? 'ninguno'}' no puede montar '${appName}' (requiere: ${required.join(', ') || 'un principal'})`,
7612
+ );
7613
+ err.code = 'WU_ACCESS_DENIED';
7614
+ err.appName = appName;
7615
+ err.required = required;
7616
+ throw err;
7617
+ }
7618
+
7013
7619
  // ── StrictMode + Suspense compat: reference counting ──
7014
7620
  // Old approach was a single 60ms grace timer cancelled by the second
7015
7621
  // mount(). That works for one mount→unmount→mount cycle but breaks
@@ -7026,41 +7632,172 @@ class WuCore {
7026
7632
  logger.wuDebug(`${appName} deferred unmount cancelled by remount`);
7027
7633
  }
7028
7634
 
7029
- // Already mounted in same container no-op (refs already incremented above)
7030
- if (this.mounted.has(appName)) {
7031
- const existing = this.mounted.get(appName);
7032
- if (existing.containerSelector === containerSelector) {
7033
- logger.wuDebug(`${appName} already mounted in ${containerSelector}`);
7034
- return;
7635
+ // Failed/cancelled mounts must release their ref, otherwise future
7636
+ // unmounts are skipped forever ("refs still active").
7637
+ let refReleased = false;
7638
+ const releaseRef = () => {
7639
+ if (refReleased) return;
7640
+ refReleased = true;
7641
+ this._releaseMountRef(appName);
7642
+ };
7643
+ // Teardowns (force unmount / deferred teardown) delete the whole ref
7644
+ // entry, taking this caller's +1 with it — re-establish it so the
7645
+ // remounted instance survives until its own unmount.
7646
+ const reclaimRef = () => {
7647
+ if (!refReleased && (this._mountRefs.get(appName) || 0) === 0) {
7648
+ this._mountRefs.set(appName, 1);
7035
7649
  }
7650
+ };
7651
+
7652
+ try {
7653
+ // Wait for any in-flight deferred teardown before evaluating mounted
7654
+ // state — otherwise we'd report "already mounted" while the teardown
7655
+ // finishes underneath and leaves the container empty. The wait is
7656
+ // bounded: a beforeUnmount/afterUnmount hook that mounts this same
7657
+ // app would otherwise form a circular teardown→hook→mount() wait.
7658
+ if (this._unmountingPromises.has(appName)) {
7659
+ await Promise.race([
7660
+ this._unmountingPromises.get(appName),
7661
+ new Promise((resolve) => setTimeout(resolve, 5000)),
7662
+ ]);
7663
+ reclaimRef();
7664
+ }
7665
+
7666
+ // Already mounted in same container → no-op (refs already incremented above)
7667
+ if (this.mounted.has(appName)) {
7668
+ const existing = this.mounted.get(appName);
7669
+ if (existing.containerSelector === containerSelector) {
7670
+ logger.wuDebug(`${appName} already mounted in ${containerSelector}`);
7671
+ return;
7672
+ }
7673
+ // Different container → destroy the old instance, then remount normally
7674
+ await this.unmount(appName, { force: true });
7675
+ reclaimRef();
7676
+ }
7677
+
7678
+ // Deduplicate concurrent mounts (StrictMode fires effect twice)
7679
+ const inflight = this._mountingPromises.get(appName);
7680
+ if (inflight) {
7681
+ if (inflight.containerSelector === containerSelector) {
7682
+ logger.wuDebug(`${appName} mount already in progress, deduplicating`);
7683
+ return await inflight.promise;
7684
+ }
7685
+ // Different container → wait for the in-flight mount to settle,
7686
+ // then remount normally (the recursive call manages its own ref)
7687
+ releaseRef();
7688
+ await inflight.promise.catch(() => {});
7689
+ return await this.mount(appName, containerSelector);
7690
+ }
7691
+
7692
+ // Check if app is in keep-alive (hidden) state
7693
+ const hiddenEntry = this.hidden.get(appName);
7694
+ if (hiddenEntry) {
7695
+ if (hiddenEntry.containerSelector === containerSelector) {
7696
+ // Same container → instant show (no reload)
7697
+ return await this.show(appName);
7698
+ }
7699
+ // Different container → destroy hidden state, remount normally
7700
+ await this._destroyHidden(appName);
7701
+ }
7702
+
7703
+ // Track mount promise for deduplication
7704
+ const mountPromise = this.mountWithRecovery(appName, containerSelector, 0);
7705
+ this._mountingPromises.set(appName, { promise: mountPromise, containerSelector });
7706
+
7707
+ try {
7708
+ return await mountPromise;
7709
+ } finally {
7710
+ this._mountingPromises.delete(appName);
7711
+ }
7712
+ } catch (error) {
7713
+ releaseRef();
7714
+ throw error;
7036
7715
  }
7716
+ }
7037
7717
 
7038
- // Deduplicate concurrent mounts (StrictMode fires effect twice)
7039
- if (this._mountingPromises.has(appName)) {
7040
- logger.wuDebug(`${appName} mount already in progress, deduplicating`);
7041
- return await this._mountingPromises.get(appName);
7718
+ /**
7719
+ * Push new props into an already-mounted micro-app WITHOUT remounting —
7720
+ * the live-props channel. Calls the app's optional `update(container, props)`
7721
+ * lifecycle slot (adapters built on createWuAdapter expose it when their
7722
+ * framework can re-render in place).
7723
+ *
7724
+ * Honest no-op semantics: returns false (and warns once) when the app is not
7725
+ * mounted or its adapter did not advertise an `update` slot — never throws,
7726
+ * never silently pretends to have updated.
7727
+ *
7728
+ * @param {string} appName - Nombre de la app montada
7729
+ * @param {Object} props - Props a fusionar/empujar a la app
7730
+ * @returns {Promise<boolean>} true si la app recibió las props
7731
+ */
7732
+ async update(appName, props = {}) {
7733
+ // Wait out any in-flight deferred teardown so we don't update an app that
7734
+ // is about to disappear (mirrors mount()'s teardown coordination).
7735
+ if (this._unmountingPromises.has(appName)) {
7736
+ await Promise.race([
7737
+ this._unmountingPromises.get(appName),
7738
+ new Promise((resolve) => setTimeout(resolve, 5000)),
7739
+ ]);
7740
+ }
7741
+
7742
+ // Re-validate AFTER the race: if the timeout won (teardown >5s) the mount
7743
+ // record may still be present mid-unmount — its container/lifecycle are
7744
+ // already torn down, so don't update it.
7745
+ if (this._unmountingPromises.has(appName)) {
7746
+ logger.wuWarn(`[Wu] update('${appName}') ignored: app is being unmounted`);
7747
+ return false;
7042
7748
  }
7043
7749
 
7044
- // Check if app is in keep-alive (hidden) state
7045
- const hiddenEntry = this.hidden.get(appName);
7046
- if (hiddenEntry) {
7047
- if (hiddenEntry.containerSelector === containerSelector) {
7048
- // Same container → instant show (no reload)
7049
- return await this.show(appName);
7050
- }
7051
- // Different container → destroy hidden state, remount normally
7052
- await this._destroyHidden(appName);
7750
+ const mounted = this.mounted.get(appName) || this.hidden.get(appName);
7751
+ if (!mounted) {
7752
+ logger.wuWarn(`[Wu] update('${appName}') ignored: app is not mounted`);
7753
+ return false;
7053
7754
  }
7054
7755
 
7055
- // Track mount promise for deduplication
7056
- const mountPromise = this.mountWithRecovery(appName, containerSelector, 0);
7057
- this._mountingPromises.set(appName, mountPromise);
7756
+ const lifecycle = mounted.lifecycle;
7757
+ if (!lifecycle || typeof lifecycle.update !== 'function') {
7758
+ logger.wuWarn(
7759
+ `[Wu] update('${appName}') is a no-op: this app's adapter does not support ` +
7760
+ `live props (no update() slot). Re-mount to change props, or use the ` +
7761
+ `event bus / store for cross-app state.`
7762
+ );
7763
+ return false;
7764
+ }
7058
7765
 
7059
7766
  try {
7060
- return await mountPromise;
7061
- } finally {
7062
- this._mountingPromises.delete(appName);
7767
+ // A beforeUpdate hook may veto the push, like every other before* phase.
7768
+ const before = await this.hooks.execute('beforeUpdate', { appName, props, mounted });
7769
+ if (before.cancelled) {
7770
+ logger.wuWarn(`[Wu] update('${appName}') cancelled by beforeUpdate hook`);
7771
+ return false;
7772
+ }
7773
+ await lifecycle.update(mounted.container, props);
7774
+ // Retain the latest props on the mount record for inspection/devtools.
7775
+ mounted.props = { ...(mounted.props || {}), ...props };
7776
+ await this.hooks.execute('afterUpdate', { appName, props });
7777
+ this.eventBus.emit('app:updated', { appName, props }, { appName });
7778
+ logger.wuDebug(`${appName} props updated`);
7779
+ return true;
7780
+ } catch (error) {
7781
+ logger.wuError(`[Wu] update('${appName}') failed:`, error);
7782
+ this.errorBoundary.handle(error, { appName, phase: 'update' });
7783
+ return false;
7784
+ }
7785
+ }
7786
+
7787
+ /**
7788
+ * Release one mount reference. Used when a mount fails or is cancelled,
7789
+ * so the ref count stays balanced with successful mounts.
7790
+ * @private
7791
+ */
7792
+ _releaseMountRef(appName) {
7793
+ const prev = this._mountRefs.get(appName) || 0;
7794
+ const next = prev - 1;
7795
+ if (next > 0) {
7796
+ this._mountRefs.set(appName, next);
7797
+ } else {
7798
+ this._mountRefs.delete(appName);
7063
7799
  }
7800
+ logger.wuDebug(`${appName} mount ref released: ${prev} → ${Math.max(0, next)}`);
7064
7801
  }
7065
7802
 
7066
7803
  /**
@@ -7079,6 +7816,7 @@ class WuCore {
7079
7816
  const beforeLoadResult = await this.hooks.execute('beforeLoad', { appName, containerSelector, attempt });
7080
7817
  if (beforeLoadResult.cancelled) {
7081
7818
  logger.wuWarn('Mount cancelled by beforeLoad hook');
7819
+ this._releaseMountRef(appName);
7082
7820
  return;
7083
7821
  }
7084
7822
 
@@ -7086,6 +7824,7 @@ class WuCore {
7086
7824
  const pluginBeforeMount = await this.pluginSystem.callHook('beforeMount', { appName, containerSelector });
7087
7825
  if (pluginBeforeMount === false) {
7088
7826
  logger.wuWarn('Mount cancelled by plugin beforeMount hook');
7827
+ this._releaseMountRef(appName);
7089
7828
  return;
7090
7829
  }
7091
7830
 
@@ -7127,6 +7866,12 @@ class WuCore {
7127
7866
  const beforeMountResult = await this.hooks.execute('beforeMount', { appName, containerSelector, sandbox, lifecycle });
7128
7867
  if (beforeMountResult.cancelled) {
7129
7868
  logger.wuWarn('Mount cancelled by beforeMount hook');
7869
+ if (sandbox.iframeSandbox) {
7870
+ sandbox.iframeSandbox.destroy();
7871
+ sandbox.iframeSandbox = null;
7872
+ }
7873
+ this.sandbox.cleanup(sandbox);
7874
+ this._releaseMountRef(appName);
7130
7875
  return;
7131
7876
  }
7132
7877
 
@@ -7173,8 +7918,12 @@ class WuCore {
7173
7918
  try {
7174
7919
  if (this.sandbox && this.sandbox.sandboxes && this.sandbox.sandboxes.has(appName)) {
7175
7920
  const sb = this.sandbox.sandboxes.get(appName);
7176
- if (sb && sb.proxySandbox) {
7177
- sb.proxySandbox.deactivate();
7921
+ if (sb && sb.iframeSandbox) {
7922
+ sb.iframeSandbox.destroy();
7923
+ sb.iframeSandbox = null;
7924
+ }
7925
+ if (sb && sb.jsSandbox && sb.jsSandbox.isActive()) {
7926
+ sb.jsSandbox.deactivate();
7178
7927
  }
7179
7928
  this.sandbox.sandboxes.delete(appName);
7180
7929
  logger.wuDebug(`Sandbox cleaned up after mount failure for ${appName}`);
@@ -7191,18 +7940,20 @@ class WuCore {
7191
7940
  container: containerSelector
7192
7941
  });
7193
7942
 
7194
- // Si el error boundary recupero el error, no necesitamos reintentar
7195
- if (errorResult.recovered) {
7196
- logger.wuDebug('Error recovered by error boundary');
7197
- return;
7198
- }
7199
-
7200
- // Recovery protocol
7201
- if (attempt < maxAttempts - 1 && errorResult.action === 'retry') {
7943
+ // Recovery protocol: handlers that ask for a retry get actual retries
7944
+ const wantsRetry = errorResult.action === 'retry' ||
7945
+ errorResult.action === 'retry-with-longer-timeout';
7946
+ if (wantsRetry && attempt < maxAttempts - 1) {
7202
7947
  logger.wuDebug('Initiating recovery protocol...');
7203
7948
 
7204
- // Clean app state
7205
- await this.appStateCleanup(appName, containerSelector);
7949
+ // Clean app state (keeping the wu.define() registration: the module
7950
+ // is cached by the browser and won't re-register on re-import)
7951
+ await this.appStateCleanup(appName, containerSelector, { preserveDefinition: true });
7952
+ // The cleanup's force-unmount wipes the ref table — restore the
7953
+ // caller's ref so the retried mount stays balanced with its unmount.
7954
+ if ((this._mountRefs.get(appName) || 0) === 0) {
7955
+ this._mountRefs.set(appName, 1);
7956
+ }
7206
7957
 
7207
7958
  // Temporal stabilization
7208
7959
  await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
@@ -7211,6 +7962,14 @@ class WuCore {
7211
7962
  return await this.mountWithRecovery(appName, containerSelector, attempt + 1);
7212
7963
  }
7213
7964
 
7965
+ // Recovered without a retry request (custom handler resolved it).
7966
+ // Nothing was mounted by us, so the ref must be released.
7967
+ if (errorResult.recovered && !wantsRetry) {
7968
+ logger.wuDebug('Error recovered by error boundary');
7969
+ this._releaseMountRef(appName);
7970
+ return;
7971
+ }
7972
+
7214
7973
  // Call plugin error hooks
7215
7974
  await this.pluginSystem.callHook('onError', { phase: 'mount', error, appName });
7216
7975
 
@@ -7221,8 +7980,10 @@ class WuCore {
7221
7980
 
7222
7981
  /**
7223
7982
  * App state cleanup: Enhanced container cleanup with framework protection
7983
+ * `preserveDefinition` keeps the wu.define() registration: a cached ES
7984
+ * module won't re-execute on re-import, so a retry could never restore it.
7224
7985
  */
7225
- async appStateCleanup(appName, containerSelector) {
7986
+ async appStateCleanup(appName, containerSelector, { preserveDefinition = false } = {}) {
7226
7987
  try {
7227
7988
  logger.wuDebug(`Starting app state cleanup for ${appName}...`);
7228
7989
 
@@ -7280,7 +8041,9 @@ class WuCore {
7280
8041
  }
7281
8042
 
7282
8043
  // Reset definition state
7283
- this.definitions.delete(appName);
8044
+ if (!preserveDefinition) {
8045
+ this.definitions.delete(appName);
8046
+ }
7284
8047
 
7285
8048
  // Clear sandbox registry
7286
8049
  if (this.sandbox && this.sandbox.sandboxes) {
@@ -7548,7 +8311,15 @@ class WuCore {
7548
8311
  }
7549
8312
 
7550
8313
  // Wait for wu.define()
7551
- await this._waitForDefine(app.name, 'strict');
8314
+ try {
8315
+ await this._waitForDefine(app.name, 'strict');
8316
+ } catch (defineError) {
8317
+ // Module imported but never called wu.define() — destroy the iframe
8318
+ // so each failed strict mount doesn't leak a hidden window.
8319
+ iframeSandbox.destroy();
8320
+ sandbox.iframeSandbox = null;
8321
+ throw defineError;
8322
+ }
7552
8323
 
7553
8324
  logger.wuDebug(`[strict] ${app.name} loaded and registered via iframe`);
7554
8325
  }
@@ -7905,18 +8676,25 @@ class WuCore {
7905
8676
  clearTimeout(this._pendingUnmounts.get(appName));
7906
8677
  }
7907
8678
 
7908
- this._pendingUnmounts.set(appName, setTimeout(async () => {
8679
+ this._pendingUnmounts.set(appName, setTimeout(() => {
7909
8680
  this._pendingUnmounts.delete(appName);
7910
8681
  // Re-check refs at fire time — caller may have re-mounted during grace
7911
8682
  if ((this._mountRefs.get(appName) || 0) > 0) return;
7912
8683
  // Re-verify: only unmount if the same mount entry is still current
7913
8684
  if (this.mounted.has(appName) && this.mounted.get(appName) === mounted) {
7914
- try {
7915
- await this._executeUnmount(appName, mounted);
7916
- this._mountRefs.delete(appName);
7917
- } catch (error) {
7918
- logger.wuError(`Deferred unmount failed for ${appName}:`, error);
7919
- }
8685
+ // Track the in-flight teardown synchronously so a concurrent mount()
8686
+ // awaits it instead of seeing a half-unmounted app as "already mounted"
8687
+ const teardown = (async () => {
8688
+ try {
8689
+ await this._executeUnmount(appName, mounted);
8690
+ this._mountRefs.delete(appName);
8691
+ } catch (error) {
8692
+ logger.wuError(`Deferred unmount failed for ${appName}:`, error);
8693
+ } finally {
8694
+ this._unmountingPromises.delete(appName);
8695
+ }
8696
+ })();
8697
+ this._unmountingPromises.set(appName, teardown);
7920
8698
  }
7921
8699
  }, 60));
7922
8700
  }
@@ -7965,6 +8743,10 @@ class WuCore {
7965
8743
  // Call plugin afterUnmount hooks
7966
8744
  await this.pluginSystem.callHook('afterUnmount', { appName });
7967
8745
 
8746
+ // Revoke this app's capability contracts with the framework-verified
8747
+ // app name (NOT from the spoofable app:unmounted bus event).
8748
+ try { this.contracts.revokeByApp(appName); } catch { /* best effort */ }
8749
+
7968
8750
  // Emit unmount event
7969
8751
  this.eventBus.emit('app:unmounted', { appName }, { appName });
7970
8752
 
@@ -8192,6 +8974,138 @@ class WuCore {
8192
8974
  };
8193
8975
  }
8194
8976
 
8977
+ /**
8978
+ * Register a capability another app can consume (typed, versioned contract).
8979
+ * @param {string} name
8980
+ * @param {object|Function} impl
8981
+ * @param {Object} [opts] - { version, shape, app }
8982
+ * @returns {{ revoke: Function }}
8983
+ * @see WuContracts#provide
8984
+ */
8985
+ provide(name, impl, opts) {
8986
+ return this.contracts.provide(name, impl, opts);
8987
+ }
8988
+
8989
+ /**
8990
+ * Consume a capability provided by another app. Returns a live proxy that
8991
+ * fails loudly on access if no provider satisfies the range (or a Promise
8992
+ * with { wait: true }).
8993
+ * @param {string} name
8994
+ * @param {string} [range='*']
8995
+ * @param {Object} [opts] - { wait, timeout }
8996
+ * @returns {Proxy|Promise<Proxy>}
8997
+ * @see WuContracts#consume
8998
+ */
8999
+ consume(name, range, opts) {
9000
+ return this.contracts.consume(name, range, opts);
9001
+ }
9002
+
9003
+ /**
9004
+ * Full diagnostic snapshot of the whole page — one structured object that
9005
+ * aggregates state scattered across mounted/hidden/definitions/apps, the
9006
+ * event bus, the store, and per-app sandbox info. The data backbone for
9007
+ * `wu.showInspector()` and the `window.__WU_DEVTOOLS__` bridge, and useful
9008
+ * on its own for debugging multi-framework pages where per-framework
9009
+ * devtools each see only their own island.
9010
+ *
9011
+ * @param {Object} [opts]
9012
+ * @param {number} [opts.events=25] - Recent events to include
9013
+ * @returns {Object} snapshot
9014
+ */
9015
+ inspect(opts = {}) {
9016
+ const { events = 25 } = opts;
9017
+
9018
+ const describeMount = (name, rec, status) => {
9019
+ const lifecycle = rec.lifecycle || this.definitions.get(name);
9020
+ return {
9021
+ name,
9022
+ status,
9023
+ framework: rec.app?.framework || rec.app?.config?.framework || null,
9024
+ containerSelector: rec.containerSelector || null,
9025
+ mountedAt: rec.timestamp || null,
9026
+ liveProps: typeof lifecycle?.update === 'function',
9027
+ props: rec.props || null,
9028
+ sandbox: this.getSandboxInfo(name),
9029
+ };
9030
+ };
9031
+
9032
+ const apps = [];
9033
+ for (const [name, rec] of this.mounted) apps.push(describeMount(name, rec, 'mounted'));
9034
+ for (const [name, rec] of this.hidden) apps.push(describeMount(name, rec, 'hidden'));
9035
+
9036
+ let eventHistory = [];
9037
+ try {
9038
+ const hist = this.eventBus.history || [];
9039
+ eventHistory = hist.slice(-events).map((e) => ({
9040
+ // WuEventBus stores the event under `name`; keep type/event fallbacks
9041
+ // for any external producer that uses a different field.
9042
+ type: e.name || e.type || e.event,
9043
+ appName: e.appName,
9044
+ timestamp: e.timestamp,
9045
+ }));
9046
+ } catch { /* bus may not expose history */ }
9047
+
9048
+ let storeSnapshot = null;
9049
+ try { storeSnapshot = this.store.get(); } catch { /* guarded */ }
9050
+
9051
+ return {
9052
+ version: this.version || null,
9053
+ timestamp: Date.now(),
9054
+ summary: {
9055
+ registered: this.apps.size,
9056
+ defined: this.definitions.size,
9057
+ mounted: this.mounted.size,
9058
+ hidden: this.hidden.size,
9059
+ },
9060
+ apps,
9061
+ defined: Array.from(this.definitions.keys()),
9062
+ registered: Array.from(this.apps.keys()),
9063
+ capabilities: (() => { try { return this.contracts.list(); } catch { return []; } })(),
9064
+ events: {
9065
+ recent: eventHistory,
9066
+ stats: (() => { try { return this.eventBus.getStats(); } catch { return null; } })(),
9067
+ },
9068
+ store: {
9069
+ snapshot: storeSnapshot,
9070
+ metrics: (() => { try { return this.store.getMetrics(); } catch { return null; } })(),
9071
+ },
9072
+ };
9073
+ }
9074
+
9075
+ /**
9076
+ * Open the visual inspector overlay (lazy-loaded, Shadow-DOM isolated so it
9077
+ * never collides with app styles). Returns a handle with .close().
9078
+ * @returns {Promise<{close: Function}|null>}
9079
+ */
9080
+ async showInspector() {
9081
+ if (typeof window === 'undefined') return null;
9082
+ const { mountInspector } = await import('./core/wu-devtools.js');
9083
+ return mountInspector(this);
9084
+ }
9085
+
9086
+ /**
9087
+ * Lazy-load and instantiate the time-travel recorder. Kept out of the main
9088
+ * bundle (like devtools/loader) — only paid for when something asks for
9089
+ * `wu.timeline`. Cached on `this._timeline`.
9090
+ * @private
9091
+ * @returns {Promise<import('./wu-timeline.js').WuTimeline>}
9092
+ */
9093
+ async _ensureTimeline() {
9094
+ if (this._timeline) return this._timeline;
9095
+ const { WuTimeline } = await import('./core/wu-timeline.js');
9096
+ this._timeline = new WuTimeline(this);
9097
+ return this._timeline;
9098
+ }
9099
+
9100
+ /** Close the inspector overlay if open. */
9101
+ async hideInspector() {
9102
+ if (typeof window === 'undefined') return;
9103
+ try {
9104
+ const { unmountInspector } = await import('./core/wu-devtools.js');
9105
+ unmountInspector();
9106
+ } catch { /* never opened */ }
9107
+ }
9108
+
8195
9109
  /**
8196
9110
  * Store methods: Convenience methods for state management
8197
9111
  */
@@ -8338,6 +9252,14 @@ class WuCore {
8338
9252
  logger.wuDebug('Destroying framework...');
8339
9253
 
8340
9254
  try {
9255
+ // Close the inspector overlay so its refresh timer doesn't outlive us
9256
+ await this.hideInspector();
9257
+
9258
+ // Stop the timeline recorder so its store/bus taps don't dangle.
9259
+ if (this._timeline) {
9260
+ try { this._timeline.stop(); } catch { /* best effort */ }
9261
+ }
9262
+
8341
9263
  // Execute beforeDestroy hooks
8342
9264
  await this.hooks.execute('beforeDestroy', {});
8343
9265
 
@@ -8350,6 +9272,7 @@ class WuCore {
8350
9272
  }
8351
9273
  this._pendingUnmounts.clear();
8352
9274
  this._mountingPromises.clear();
9275
+ this._unmountingPromises.clear();
8353
9276
 
8354
9277
  // Cancel and reject any pending define() waiters
8355
9278
  for (const [, waiter] of this._defineWaiters) {
@@ -8382,6 +9305,7 @@ class WuCore {
8382
9305
  this.errorBoundary.cleanup();
8383
9306
  this.hooks.cleanup();
8384
9307
  this.prefetcher.cleanup();
9308
+ this.contracts.cleanup();
8385
9309
 
8386
9310
  // Limpiar registros
8387
9311
  this.apps.clear();
@@ -8599,75 +9523,160 @@ if (typeof window !== 'undefined') {
8599
9523
  console.warn('[Wu Framework] window.wu already exists and is not a Wu Framework instance. Overwriting. Use Symbol.for("wu-framework") for collision-safe access.');
8600
9524
  }
8601
9525
  window.wu = wu;
9526
+ }
8602
9527
 
8603
- if (!wu.version) {
8604
- // "2.1.1" is replaced at build time with package.json version.
8605
- // Fallback handles raw-source imports (no bundler).
8606
- wu.version = "2.1.1" ;
8607
- wu.info = {
8608
- name: 'Wu Framework',
8609
- description: 'Universal Microfrontends',
8610
- features: ['Framework Agnostic', 'Zero Config', 'Shadow DOM Isolation', 'Runtime Loading']
8611
- };
8612
- }
9528
+ // Augment the instance everywhere (browser AND Node/SSR) — the d.ts declares
9529
+ // these members unconditionally on WuCore, so they must exist in any runtime.
9530
+ if (!wu.version) {
9531
+ // "2.6.0" is replaced at build time with package.json version.
9532
+ // Fallback handles raw-source imports (no bundler).
9533
+ wu.version = "2.6.0" ;
9534
+ wu.info = {
9535
+ name: 'Wu Framework',
9536
+ description: 'Universal Microfrontends',
9537
+ features: ['Framework Agnostic', 'Zero Config', 'Shadow DOM Isolation', 'Runtime Loading']
9538
+ };
9539
+ }
8613
9540
 
8614
- // Event Bus shortcuts on window.wu
8615
- if (!wu.emit) {
8616
- wu.emit = (event, data, opts) => wu.eventBus.emit(event, data, opts);
8617
- wu.on = (event, cb) => wu.eventBus.on(event, cb);
8618
- wu.once = (event, cb) => wu.eventBus.once(event, cb);
8619
- wu.off = (event, cb) => wu.eventBus.off(event, cb);
8620
- }
9541
+ // Event Bus shortcuts on wu
9542
+ if (!wu.emit) {
9543
+ wu.emit = (event, data, opts) => wu.eventBus.emit(event, data, opts);
9544
+ wu.on = (event, cb) => wu.eventBus.on(event, cb);
9545
+ wu.once = (event, cb) => wu.eventBus.once(event, cb);
9546
+ wu.off = (event, cb) => wu.eventBus.off(event, cb);
9547
+ }
8621
9548
 
8622
- // Prefetch shortcuts on window.wu
8623
- if (!wu.prefetch) {
8624
- wu.prefetch = (appNames, opts) => wu.prefetcher.prefetch(appNames, opts);
8625
- wu.prefetchAll = (opts) => wu.prefetcher.prefetchAll(opts);
8626
- }
9549
+ // Prefetch shortcuts on wu
9550
+ if (!wu.prefetch) {
9551
+ wu.prefetch = (appNames, opts) => wu.prefetcher.prefetch(appNames, opts);
9552
+ wu.prefetchAll = (opts) => wu.prefetcher.prefetchAll(opts);
9553
+ }
8627
9554
 
8628
- // Override shortcuts on window.wu
8629
- if (!wu.override) {
8630
- wu.override = (name, url, opts) => wu.overrides.set(name, url, opts);
8631
- wu.removeOverride = (name) => wu.overrides.remove(name);
8632
- wu.getOverrides = () => wu.overrides.getAll();
8633
- wu.clearOverrides = () => wu.overrides.clearAll();
8634
- }
9555
+ // Override shortcuts on wu
9556
+ if (!wu.override) {
9557
+ wu.override = (name, url, opts) => wu.overrides.set(name, url, opts);
9558
+ wu.removeOverride = (name) => wu.overrides.remove(name);
9559
+ wu.getOverrides = () => wu.overrides.getAll();
9560
+ wu.clearOverrides = () => wu.overrides.clearAll();
9561
+ }
8635
9562
 
8636
- // Log control: window.wu.silence() / window.wu.verbose()
8637
- if (!wu.silence) {
8638
- wu.silence = async () => { const { silenceAllLogs } = await Promise.resolve().then(function () { return wuLogger; }); silenceAllLogs(); };
8639
- wu.verbose = async () => { const { enableAllLogs } = await Promise.resolve().then(function () { return wuLogger; }); enableAllLogs(); };
8640
- }
9563
+ // Capability-contract shortcuts on wu (v2.5+)
9564
+ if (!wu.provide) {
9565
+ wu.provide = (name, impl, opts) => wu.contracts.provide(name, impl, opts);
9566
+ wu.consume = (name, range, opts) => wu.contracts.consume(name, range, opts);
9567
+ }
8641
9568
 
8642
- // AI integration lazy-loaded chunk + transparent proxy.
8643
- // The wu-ai subsystem (~80–100 KB minified) lives in a separate chunk
8644
- // and is only fetched on first access of `wu.ai.*`. Chainable config
8645
- // methods (provider/action/trigger/...) queue calls sync and return the
8646
- // proxy. Async methods (send/stream/agent/...) await the import + delegate.
8647
- // Use `await wu.aiReady()` to force load + get the real instance.
8648
- if (!wu.ai) {
8649
- setupLazyAi(wu);
8650
- }
9569
+ // Log control: wu.silence() / wu.verbose()
9570
+ if (!wu.silence) {
9571
+ wu.silence = async () => { const { silenceAllLogs } = await import('./core/wu-logger.js'); silenceAllLogs(); };
9572
+ wu.verbose = async () => { const { enableAllLogs } = await import('./core/wu-logger.js'); enableAllLogs(); };
9573
+ }
8651
9574
 
8652
- // MCP bridgeconnects to wu-mcp-server for AI agent control
8653
- if (!wu.mcp) {
8654
- let _mcpBridge = null;
8655
- wu.mcp = {
8656
- async connect(url = 'ws://localhost:19100', options = {}) {
8657
- if (!_mcpBridge) {
8658
- const { createMcpBridge } = await import('./core/wu-mcp-bridge.js');
8659
- _mcpBridge = createMcpBridge(wu);
8660
- }
8661
- _mcpBridge.connect(url, options);
8662
- },
8663
- disconnect() {
8664
- _mcpBridge?.disconnect();
8665
- },
8666
- isConnected() {
8667
- return _mcpBridge?.isConnected() || false;
8668
- },
8669
- };
8670
- }
9575
+ // AI integrationlazy-loaded chunk + transparent proxy.
9576
+ // The wu-ai subsystem (~80–100 KB minified) lives in a separate chunk
9577
+ // and is only fetched on first access of `wu.ai.*`. Chainable config
9578
+ // methods (provider/action/trigger/...) queue calls sync and return the
9579
+ // proxy. Async methods (send/stream/agent/...) await the import + delegate.
9580
+ // Use `await wu.aiReady()` to force load + get the real instance.
9581
+ if (!wu.ai) {
9582
+ setupLazyAi(wu);
9583
+ }
9584
+
9585
+ // DevTools bridge — a tiny always-present global so external inspectors (and
9586
+ // the built-in overlay) can detect Wu and pull a full-page snapshot. The
9587
+ // visual overlay itself is a lazy chunk, loaded only on show().
9588
+ if (typeof window !== 'undefined' && !window.__WU_DEVTOOLS__) {
9589
+ window.__WU_DEVTOOLS__ = {
9590
+ version: wu.version,
9591
+ isWu: true,
9592
+ inspect: (opts) => wu.inspect(opts),
9593
+ show: () => wu.showInspector(),
9594
+ hide: () => wu.hideInspector(),
9595
+ subscribe: (cb) => wu.eventBus.on('*', cb),
9596
+ };
9597
+ }
9598
+
9599
+ // MCP bridge — connects to wu-mcp-server for AI agent control
9600
+ if (!wu.mcp) {
9601
+ let _mcpBridge = null;
9602
+ wu.mcp = {
9603
+ async connect(url = 'ws://localhost:19100', options = {}) {
9604
+ if (!_mcpBridge) {
9605
+ const { createMcpBridge } = await import('./core/wu-mcp-bridge.js');
9606
+ _mcpBridge = createMcpBridge(wu);
9607
+ }
9608
+ _mcpBridge.connect(url, options);
9609
+ },
9610
+ disconnect() {
9611
+ _mcpBridge?.disconnect();
9612
+ },
9613
+ isConnected() {
9614
+ return _mcpBridge?.isConnected() || false;
9615
+ },
9616
+ };
9617
+ }
9618
+
9619
+ // Timeline — cross-framework time travel. Lazy chunk (loaded on first
9620
+ // `wu.timeline.*` call). The facade is synchronous and chainable for the
9621
+ // fire-and-forget controls (record/stop/clear) and returns Promises for the
9622
+ // seek family, mirroring the real WuTimeline API. `status()` answers without
9623
+ // forcing a load so devtools can poll it cheaply.
9624
+ //
9625
+ // Honest note: the recorder is a lazy chunk, so record() ARMS asynchronously —
9626
+ // store writes made before the chunk resolves are not journaled. record()/
9627
+ // stop() intent is tracked so a stop() issued before load cancels a pending
9628
+ // record() (no "recording after stop" surprise). For programmatic recording
9629
+ // that must capture the very next write, `await wu.timelineReady()` first
9630
+ // (mirrors wu.aiReady) — after that, record() arms synchronously. In the UI
9631
+ // path the chunk is already loaded (opening the inspector loads it), so the
9632
+ // Rec button arms immediately.
9633
+ if (!wu.timeline) {
9634
+ let _tl = null;
9635
+ let _loading = null;
9636
+ let _wantRecording = false;
9637
+ let _recordOpts;
9638
+ const ensure = () => {
9639
+ if (_tl) return Promise.resolve(_tl);
9640
+ if (!_loading) _loading = wu._ensureTimeline().then((t) => {
9641
+ _tl = t;
9642
+ // Apply the latest intent expressed before the chunk arrived.
9643
+ if (_wantRecording && !t.status().recording) t.record(_recordOpts);
9644
+ return t;
9645
+ });
9646
+ return _loading;
9647
+ };
9648
+ const NOT_LOADED = Object.freeze({
9649
+ loaded: false, recording: false, live: true, position: 0, length: 0,
9650
+ site: null, lamport: 0, snapshots: 0,
9651
+ });
9652
+ const facade = {
9653
+ record(opts) {
9654
+ _wantRecording = true; _recordOpts = opts;
9655
+ if (_tl) { if (!_tl.status().recording) _tl.record(opts); } else ensure();
9656
+ return facade;
9657
+ },
9658
+ stop() {
9659
+ _wantRecording = false; // cancels a record() still pending load
9660
+ if (_tl) _tl.stop();
9661
+ return facade;
9662
+ },
9663
+ clear() { ensure().then((t) => t.clear()); return facade; },
9664
+ seek(pos, o) { return ensure().then((t) => t.seek(pos, o)); },
9665
+ live() { return ensure().then((t) => t.live()); },
9666
+ stepBack() { return ensure().then((t) => t.stepBack()); },
9667
+ stepForward() { return ensure().then((t) => t.stepForward()); },
9668
+ export() { return ensure().then((t) => t.export()); },
9669
+ import(data) { return ensure().then((t) => t.import(data)); },
9670
+ ingest(entries) { return ensure().then((t) => t.ingest(entries)); },
9671
+ entries() { return _tl ? _tl.entries() : []; },
9672
+ status() { return _tl ? _tl.status() : { ...NOT_LOADED }; },
9673
+ get loaded() { return !!_tl; },
9674
+ get instance() { return _tl; },
9675
+ };
9676
+ wu.timeline = facade;
9677
+ // Warm-up: await it to force the chunk load + apply pending record intent,
9678
+ // then arm recording synchronously before writes. Resolves the WuTimeline.
9679
+ wu.timelineReady = ensure;
8671
9680
  }
8672
9681
  var wu_default = wu;
8673
9682
 
@@ -8694,6 +9703,8 @@ const off = (event, cb) => wu.eventBus.off(event, cb);
8694
9703
  const getState = (path) => wu.store.get(path);
8695
9704
  const setState = (path, value) => wu.store.set(path, value);
8696
9705
  const onStateChange = (pattern, cb) => wu.store.on(pattern, cb);
9706
+ // Real-time collaborative sync (v2.4+) — lazy CRDT layer over the store.
9707
+ const syncStore = (opts) => wu.store.sync(opts);
8697
9708
 
8698
9709
  // Performance
8699
9710
  const startMeasure = (name, app) => wu.performance.startMeasure(name, app);
@@ -8714,6 +9725,16 @@ const clearOverrides = () => wu.clearOverrides();
8714
9725
  const usePlugin = (plugin, opts) => wu.pluginSystem.use(plugin, opts);
8715
9726
  const useHook = (phase, middleware, opts) => wu.hooks.use(phase, middleware, opts);
8716
9727
 
9728
+ // Capability contracts (v2.5+)
9729
+ const provide = (name, impl, opts) => wu.contracts.provide(name, impl, opts);
9730
+ const consume = (name, range, opts) => wu.contracts.consume(name, range, opts);
9731
+
9732
+ // RBAC: aislamiento rol↔módulo (v2.6+). `setPrincipal` fija quién usa el sistema;
9733
+ // las apps que declaran `roles` solo se montan si el principal está autorizado.
9734
+ const setPrincipal = (principal) => wu.setPrincipal(principal);
9735
+ const getPrincipal = () => wu.getPrincipal();
9736
+ const can = (appName) => wu.can(appName);
9737
+
8717
9738
  // --- AI subsystem ---
8718
9739
  // AI classes were previously re-exported from this entry point. Doing so anchored
8719
9740
  // the ~80 KB AI chunk to the main bundle. As of v2.0, AI is loaded lazily via the
@@ -8726,5 +9747,5 @@ const useHook = (phase, middleware, opts) => wu.hooks.use(phase, middleware, opt
8726
9747
  //
8727
9748
  // For runtime use, prefer `wu.ai.*` — it auto-loads the chunk on first method call.
8728
9749
 
8729
- export { WuApp, WuCache, WuCore, WuErrorBoundary, WuEventBus, WuLifecycleHooks, WuLoadingStrategy, WuManifest, WuOverrides, WuPerformance, WuPluginSystem, WuPrefetch, WuProxySandbox, WuSandbox, WuStore, WuStyleBridge, app, clearOverrides, createConditionalHook, createGuardHook, createPlugin, createSimpleHook, createTimedHook, createTransformHook, wu_default as default, define, destroy, emit, enableAllLogs, endMeasure, generatePerformanceReport, getOverrides, getState, hide, init, isHidden, mount, normalizeStyleMode, off, on, onStateChange, once, override, prefetch, prefetchAll, removeOverride, setState, show, silenceAllLogs, startMeasure, store, unmount, useHook, usePlugin, wu };
9750
+ export { WuApp, WuCache, WuContracts, WuCore, WuErrorBoundary, WuEventBus, WuLifecycleHooks, WuLoadingStrategy, WuManifest, WuOverrides, WuPerformance, WuPluginSystem, WuPrefetch, WuProxySandbox, WuSandbox, WuStore, WuStyleBridge, app, can, clearOverrides, compareVersions, consume, createConditionalHook, createGuardHook, createPlugin, createSimpleHook, createTimedHook, createTransformHook, wu_default as default, define, destroy, emit, endMeasure, generatePerformanceReport, getOverrides, getPrincipal, getState, hide, init, isHidden, mount, normalizeStyleMode, off, on, onStateChange, once, override, parseVersion, prefetch, prefetchAll, provide, removeOverride, satisfies, setPrincipal, setState, show, startMeasure, store, syncStore, unmount, useHook, usePlugin, wu };
8730
9751
  //# sourceMappingURL=wu-framework.dev.js.map