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.
- package/README.md +6 -1
- package/dist/adapters/alpine/index.d.ts +1 -1
- package/dist/adapters/angular/index.d.ts +1 -1
- package/dist/adapters/htmx/index.d.ts +1 -1
- package/dist/adapters/lit/index.d.ts +1 -1
- package/dist/adapters/lit/index.js +2 -2
- package/dist/adapters/lit/index.js.map +1 -1
- package/dist/adapters/preact/index.d.ts +1 -1
- package/dist/adapters/preact/index.js +1 -1
- package/dist/adapters/preact/index.js.map +1 -1
- package/dist/adapters/qwik/index.d.ts +3 -10
- package/dist/adapters/qwik/index.js +1 -1
- package/dist/adapters/qwik/index.js.map +1 -1
- package/dist/adapters/react/index.js +1 -1
- package/dist/adapters/react/index.js.map +1 -1
- package/dist/adapters/shared.d.ts +44 -0
- package/dist/adapters/shared.js +1 -1
- package/dist/adapters/shared.js.map +1 -1
- package/dist/adapters/solid/index.d.ts +1 -1
- package/dist/adapters/solid/index.js +1 -1
- package/dist/adapters/solid/index.js.map +1 -1
- package/dist/adapters/stencil/index.d.ts +1 -1
- package/dist/adapters/stimulus/index.d.ts +1 -1
- package/dist/adapters/svelte/index.d.ts +1 -1
- package/dist/adapters/svelte/index.js +1 -1
- package/dist/adapters/svelte/index.js.map +1 -1
- package/dist/adapters/vanilla/index.d.ts +1 -1
- package/dist/adapters/vanilla/index.js +1 -1
- package/dist/adapters/vanilla/index.js.map +1 -1
- package/dist/adapters/vue/index.js +1 -1
- package/dist/adapters/vue/index.js.map +1 -1
- package/dist/ai/wu-ai.js +1 -1
- package/dist/ai/wu-ai.js.map +1 -1
- package/dist/core/wu-devtools.js +2 -0
- package/dist/core/wu-devtools.js.map +1 -0
- package/dist/core/wu-html-parser.js +1 -1
- package/dist/core/wu-html-parser.js.map +1 -1
- package/dist/core/wu-iframe-sandbox.js +1 -1
- package/dist/core/wu-iframe-sandbox.js.map +1 -1
- package/dist/core/wu-loader.js +1 -1
- package/dist/core/wu-loader.js.map +1 -1
- package/dist/core/wu-logger.js +2 -0
- package/dist/core/wu-logger.js.map +1 -0
- package/dist/core/wu-mcp-bridge.js +1 -1
- package/dist/core/wu-mcp-bridge.js.map +1 -1
- package/dist/core/wu-script-executor.js +1 -1
- package/dist/core/wu-script-executor.js.map +1 -1
- package/dist/core/wu-store-sync.js +2 -0
- package/dist/core/wu-store-sync.js.map +1 -0
- package/dist/core/wu-timeline.js +2 -0
- package/dist/core/wu-timeline.js.map +1 -0
- package/dist/index.d.cts +759 -0
- package/dist/index.d.ts +315 -1
- package/dist/wu-ai-browser-primitives-CaUCk1Xl.js +2 -0
- package/dist/wu-ai-browser-primitives-CaUCk1Xl.js.map +1 -0
- package/dist/wu-framework.cjs +3 -0
- package/dist/wu-framework.cjs.map +1 -0
- package/dist/wu-framework.dev.js +1296 -275
- package/dist/wu-framework.dev.js.map +1 -1
- package/dist/wu-framework.esm.js +2 -2
- package/dist/wu-framework.esm.js.map +1 -1
- package/dist/wu-framework.umd.js +2 -2
- package/dist/wu-framework.umd.js.map +1 -1
- package/integrations/astro/WuApp.astro +16 -11
- package/integrations/astro/WuShell.astro +11 -3
- package/package.json +14 -6
- package/dist/wu-ai-browser-primitives-BDKXJlwc.js +0 -2
- package/dist/wu-ai-browser-primitives-BDKXJlwc.js.map +0 -1
- package/dist/wu-framework.cjs.js +0 -3
- package/dist/wu-framework.cjs.js.map +0 -1
- package/dist/wu-logger-fJfUHBGA.js +0 -2
- package/dist/wu-logger-fJfUHBGA.js.map +0 -1
package/dist/wu-framework.dev.js
CHANGED
|
@@ -1,150 +1,8 @@
|
|
|
1
|
-
/*! wu-framework v2.
|
|
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 =
|
|
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
|
-
//
|
|
3265
|
-
this.
|
|
3266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
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
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
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
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
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
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
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
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
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.
|
|
7177
|
-
sb.
|
|
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
|
-
//
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
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
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
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
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
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
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
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
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
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
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
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
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
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
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
9575
|
+
// AI integration — lazy-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,
|
|
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
|