wu-framework 1.1.16 → 1.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +39 -39
- package/README.md +440 -408
- package/dist/wu-framework.cjs.js +1 -1
- package/dist/wu-framework.cjs.js.map +1 -1
- package/dist/wu-framework.dev.js +446 -81
- package/dist/wu-framework.dev.js.map +1 -1
- package/dist/wu-framework.esm.js +1 -1
- package/dist/wu-framework.esm.js.map +1 -1
- package/dist/wu-framework.umd.js +1 -1
- package/dist/wu-framework.umd.js.map +1 -1
- package/integrations/astro/README.md +127 -127
- package/integrations/astro/WuApp.astro +63 -63
- package/integrations/astro/WuShell.astro +39 -39
- package/integrations/astro/index.js +68 -68
- package/integrations/astro/package.json +38 -38
- package/integrations/astro/types.d.ts +53 -53
- package/package.json +7 -2
- package/src/adapters/svelte/index.js +1 -1
- package/src/adapters/vanilla/index.js +1 -1
- package/src/core/wu-cache.js +24 -3
- package/src/core/wu-core.js +15 -1
- package/src/core/wu-error-boundary.js +17 -3
- package/src/core/wu-event-bus.js +43 -1
- package/src/core/wu-html-parser.js +13 -4
- package/src/core/wu-loader.js +162 -50
- package/src/core/wu-logger.js +21 -13
- package/src/core/wu-manifest.js +23 -0
- package/src/core/wu-plugin.js +57 -4
- package/src/core/wu-proxy-sandbox.js +2 -1
- package/src/core/wu-script-executor.js +48 -0
- package/src/core/wu-store.js +13 -3
- package/src/index.d.ts +317 -0
- package/src/index.js +11 -1
package/dist/wu-framework.dev.js
CHANGED
|
@@ -24,19 +24,27 @@ class WuLogger {
|
|
|
24
24
|
* Detectar si estamos en desarrollo
|
|
25
25
|
*/
|
|
26
26
|
detectEnvironment() {
|
|
27
|
-
//
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
// 1. Explicit flag takes priority
|
|
28
|
+
if (typeof window !== 'undefined' && window.WU_DEBUG === true) return true;
|
|
29
|
+
if (typeof window !== 'undefined' && window.WU_DEBUG === false) return false;
|
|
30
|
+
|
|
31
|
+
// 2. NODE_ENV check (works in bundlers and Node)
|
|
32
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return false;
|
|
33
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') return true;
|
|
34
|
+
|
|
35
|
+
// 3. Browser heuristics (only if window exists)
|
|
36
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
37
|
+
const hostname = window.location.hostname;
|
|
38
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]') return true;
|
|
39
|
+
|
|
40
|
+
// URL param override
|
|
41
|
+
try {
|
|
42
|
+
if (new URLSearchParams(window.location.search).has('wu-debug')) return true;
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 4. Default: assume production
|
|
47
|
+
return false;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
@@ -136,69 +144,178 @@ var wuLogger = /*#__PURE__*/Object.freeze({
|
|
|
136
144
|
});
|
|
137
145
|
|
|
138
146
|
/**
|
|
139
|
-
*
|
|
147
|
+
* WU-LOADER: SISTEMA DE CARGA DINAMICA UNIVERSAL
|
|
140
148
|
* Carga aplicaciones y componentes sin depender del framework
|
|
149
|
+
*
|
|
150
|
+
* Cache strategy: LRU with TTL eviction.
|
|
151
|
+
* Entries track lastAccess time. When the cache reaches maxCacheSize,
|
|
152
|
+
* the least recently accessed entry is evicted. Entries older than
|
|
153
|
+
* cacheTTL are treated as stale and removed on access or eviction.
|
|
141
154
|
*/
|
|
142
155
|
|
|
143
156
|
|
|
157
|
+
/**
|
|
158
|
+
* @typedef {Object} WuLoaderOptions
|
|
159
|
+
* @property {number} [maxCacheSize=50] - Maximum cache entries
|
|
160
|
+
* @property {number} [cacheTTL=1800000] - Cache TTL in ms (default 30min)
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @typedef {Object} WuLoaderStats
|
|
165
|
+
* @property {number} cached - Number of cached entries
|
|
166
|
+
* @property {number} loading - Number of in-flight loads
|
|
167
|
+
* @property {number} maxCacheSize - Max cache size setting
|
|
168
|
+
* @property {number} cacheTTL - Cache TTL setting
|
|
169
|
+
* @property {string[]} cacheKeys - Cached URL keys
|
|
170
|
+
*/
|
|
171
|
+
|
|
144
172
|
class WuLoader {
|
|
145
|
-
|
|
173
|
+
/**
|
|
174
|
+
* @param {Object} options
|
|
175
|
+
* @param {number} [options.maxCacheSize=50] - Maximum number of entries in the cache
|
|
176
|
+
* @param {number} [options.cacheTTL=1800000] - Time-to-live for cache entries in ms (default 30 minutes)
|
|
177
|
+
*/
|
|
178
|
+
constructor(options = {}) {
|
|
179
|
+
this.maxCacheSize = options.maxCacheSize ?? 50;
|
|
180
|
+
this.cacheTTL = options.cacheTTL ?? 1800000;
|
|
146
181
|
this.cache = new Map();
|
|
147
182
|
this.loadingPromises = new Map();
|
|
148
183
|
|
|
149
|
-
logger.debug('[WuLoader]
|
|
184
|
+
logger.debug('[WuLoader] Dynamic loader initialized');
|
|
150
185
|
}
|
|
151
186
|
|
|
152
187
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* @param {
|
|
156
|
-
* @returns {string}
|
|
188
|
+
* Read from cache with TTL validation and LRU access tracking.
|
|
189
|
+
* Returns undefined if the entry does not exist or has expired.
|
|
190
|
+
* @param {string} key
|
|
191
|
+
* @returns {string|undefined}
|
|
192
|
+
*/
|
|
193
|
+
_cacheGet(key) {
|
|
194
|
+
if (!this.cache.has(key)) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const entry = this.cache.get(key);
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
|
|
201
|
+
if (now - entry.timestamp > this.cacheTTL) {
|
|
202
|
+
this.cache.delete(key);
|
|
203
|
+
logger.debug(`[WuLoader] Cache expired for: ${key}`);
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Promote: delete and re-insert so iteration order reflects recency.
|
|
208
|
+
// Map iteration order in JS follows insertion order, so the oldest
|
|
209
|
+
// inserted entry is always first -- exactly what we need for LRU eviction.
|
|
210
|
+
this.cache.delete(key);
|
|
211
|
+
entry.lastAccess = now;
|
|
212
|
+
this.cache.set(key, entry);
|
|
213
|
+
|
|
214
|
+
return entry.code;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Write to cache. Evicts stale and LRU entries before inserting.
|
|
219
|
+
* @param {string} key
|
|
220
|
+
* @param {string} code
|
|
221
|
+
*/
|
|
222
|
+
_cacheSet(key, code) {
|
|
223
|
+
// If the key already exists, remove it first so re-insertion
|
|
224
|
+
// moves it to the end (most-recently-used position).
|
|
225
|
+
if (this.cache.has(key)) {
|
|
226
|
+
this.cache.delete(key);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this._evictIfNeeded();
|
|
230
|
+
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
this.cache.set(key, {
|
|
233
|
+
code,
|
|
234
|
+
timestamp: now,
|
|
235
|
+
lastAccess: now
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Evict entries until cache is below maxCacheSize.
|
|
241
|
+
*
|
|
242
|
+
* Two-pass strategy:
|
|
243
|
+
* 1. Remove all expired entries (TTL exceeded).
|
|
244
|
+
* 2. If still at capacity, remove the least recently accessed entry.
|
|
245
|
+
* Because Map preserves insertion order and _cacheGet promotes on
|
|
246
|
+
* access, the first key from the iterator is always the LRU entry.
|
|
247
|
+
*/
|
|
248
|
+
_evictIfNeeded() {
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
|
|
251
|
+
// Pass 1: purge expired entries
|
|
252
|
+
for (const [key, entry] of this.cache) {
|
|
253
|
+
if (now - entry.timestamp > this.cacheTTL) {
|
|
254
|
+
this.cache.delete(key);
|
|
255
|
+
logger.debug(`[WuLoader] Evicted expired entry: ${key}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Pass 2: evict LRU entries until we are under the limit
|
|
260
|
+
while (this.cache.size >= this.maxCacheSize) {
|
|
261
|
+
// Map.keys().next() gives us the oldest-inserted key (LRU)
|
|
262
|
+
const oldestKey = this.cache.keys().next().value;
|
|
263
|
+
this.cache.delete(oldestKey);
|
|
264
|
+
logger.debug(`[WuLoader] Evicted LRU entry: ${oldestKey}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Cargar aplicacion completa
|
|
270
|
+
* @param {string} appUrl - URL base de la aplicacion
|
|
271
|
+
* @param {Object} manifest - Manifest de la aplicacion
|
|
272
|
+
* @returns {string} Codigo JavaScript de la aplicacion
|
|
157
273
|
*/
|
|
158
274
|
async loadApp(appUrl, manifest) {
|
|
159
275
|
const entryFile = manifest?.entry || 'index.js';
|
|
160
276
|
const fullUrl = `${appUrl}/${entryFile}`;
|
|
161
277
|
|
|
162
|
-
logger.debug(`[WuLoader]
|
|
278
|
+
logger.debug(`[WuLoader] Loading app from: ${fullUrl}`);
|
|
163
279
|
|
|
164
280
|
try {
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
281
|
+
// Check cache with TTL and LRU tracking
|
|
282
|
+
const cached = this._cacheGet(fullUrl);
|
|
283
|
+
if (cached !== undefined) {
|
|
284
|
+
logger.debug(`[WuLoader] Cache hit for: ${fullUrl}`);
|
|
285
|
+
return cached;
|
|
169
286
|
}
|
|
170
287
|
|
|
171
|
-
//
|
|
288
|
+
// Check if already loading
|
|
172
289
|
if (this.loadingPromises.has(fullUrl)) {
|
|
173
|
-
logger.debug(`[WuLoader]
|
|
290
|
+
logger.debug(`[WuLoader] Loading in progress for: ${fullUrl}`);
|
|
174
291
|
return await this.loadingPromises.get(fullUrl);
|
|
175
292
|
}
|
|
176
293
|
|
|
177
|
-
//
|
|
294
|
+
// Create loading promise
|
|
178
295
|
const loadingPromise = this.fetchCode(fullUrl);
|
|
179
296
|
this.loadingPromises.set(fullUrl, loadingPromise);
|
|
180
297
|
|
|
181
298
|
const code = await loadingPromise;
|
|
182
299
|
|
|
183
|
-
//
|
|
300
|
+
// Clean up loading promise and cache result
|
|
184
301
|
this.loadingPromises.delete(fullUrl);
|
|
185
|
-
this.
|
|
302
|
+
this._cacheSet(fullUrl, code);
|
|
186
303
|
|
|
187
|
-
logger.debug(`[WuLoader]
|
|
304
|
+
logger.debug(`[WuLoader] App loaded successfully: ${fullUrl}`);
|
|
188
305
|
return code;
|
|
189
306
|
|
|
190
307
|
} catch (error) {
|
|
191
308
|
this.loadingPromises.delete(fullUrl);
|
|
192
|
-
console.error(`[WuLoader]
|
|
309
|
+
console.error(`[WuLoader] Failed to load app: ${fullUrl}`, error);
|
|
193
310
|
throw new Error(`Failed to load app from ${fullUrl}: ${error.message}`);
|
|
194
311
|
}
|
|
195
312
|
}
|
|
196
313
|
|
|
197
314
|
/**
|
|
198
|
-
* Cargar componente
|
|
199
|
-
* @param {string} appUrl - URL base de la
|
|
315
|
+
* Cargar componente especifico
|
|
316
|
+
* @param {string} appUrl - URL base de la aplicacion
|
|
200
317
|
* @param {string} componentPath - Ruta del componente
|
|
201
|
-
* @returns {Function}
|
|
318
|
+
* @returns {Function} Funcion del componente
|
|
202
319
|
*/
|
|
203
320
|
async loadComponent(appUrl, componentPath) {
|
|
204
321
|
// Normalizar ruta del componente
|
|
@@ -212,13 +329,13 @@ class WuLoader {
|
|
|
212
329
|
|
|
213
330
|
const fullUrl = `${appUrl}/${normalizedPath}`;
|
|
214
331
|
|
|
215
|
-
logger.debug(`[WuLoader]
|
|
332
|
+
logger.debug(`[WuLoader] Loading component from: ${fullUrl}`);
|
|
216
333
|
|
|
217
334
|
try {
|
|
218
|
-
// Cargar
|
|
335
|
+
// Cargar codigo del componente
|
|
219
336
|
const code = await this.loadCode(fullUrl);
|
|
220
337
|
|
|
221
|
-
// Crear
|
|
338
|
+
// Crear funcion que retorna el componente
|
|
222
339
|
const componentFunction = new Function('require', 'module', 'exports', `
|
|
223
340
|
${code}
|
|
224
341
|
return typeof module.exports === 'function' ? module.exports :
|
|
@@ -235,39 +352,40 @@ class WuLoader {
|
|
|
235
352
|
|
|
236
353
|
const component = componentFunction(fakeRequire, fakeModule, fakeModule.exports);
|
|
237
354
|
|
|
238
|
-
logger.debug(`[WuLoader]
|
|
355
|
+
logger.debug(`[WuLoader] Component loaded: ${componentPath}`);
|
|
239
356
|
return component;
|
|
240
357
|
|
|
241
358
|
} catch (error) {
|
|
242
|
-
console.error(`[WuLoader]
|
|
359
|
+
console.error(`[WuLoader] Failed to load component: ${componentPath}`, error);
|
|
243
360
|
throw new Error(`Failed to load component ${componentPath}: ${error.message}`);
|
|
244
361
|
}
|
|
245
362
|
}
|
|
246
363
|
|
|
247
364
|
/**
|
|
248
|
-
* Cargar
|
|
365
|
+
* Cargar codigo con cache
|
|
249
366
|
* @param {string} url - URL del archivo
|
|
250
|
-
* @returns {string}
|
|
367
|
+
* @returns {string} Codigo JavaScript
|
|
251
368
|
*/
|
|
252
369
|
async loadCode(url) {
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
370
|
+
// Check cache with TTL and LRU tracking
|
|
371
|
+
const cached = this._cacheGet(url);
|
|
372
|
+
if (cached !== undefined) {
|
|
373
|
+
return cached;
|
|
256
374
|
}
|
|
257
375
|
|
|
258
|
-
//
|
|
376
|
+
// Check if already loading
|
|
259
377
|
if (this.loadingPromises.has(url)) {
|
|
260
378
|
return await this.loadingPromises.get(url);
|
|
261
379
|
}
|
|
262
380
|
|
|
263
|
-
//
|
|
381
|
+
// Create loading promise
|
|
264
382
|
const loadingPromise = this.fetchCode(url);
|
|
265
383
|
this.loadingPromises.set(url, loadingPromise);
|
|
266
384
|
|
|
267
385
|
try {
|
|
268
386
|
const code = await loadingPromise;
|
|
269
387
|
this.loadingPromises.delete(url);
|
|
270
|
-
this.
|
|
388
|
+
this._cacheSet(url, code);
|
|
271
389
|
return code;
|
|
272
390
|
} catch (error) {
|
|
273
391
|
this.loadingPromises.delete(url);
|
|
@@ -276,9 +394,9 @@ class WuLoader {
|
|
|
276
394
|
}
|
|
277
395
|
|
|
278
396
|
/**
|
|
279
|
-
* Realizar fetch del
|
|
397
|
+
* Realizar fetch del codigo
|
|
280
398
|
* @param {string} url - URL del archivo
|
|
281
|
-
* @returns {string}
|
|
399
|
+
* @returns {string} Codigo JavaScript
|
|
282
400
|
*/
|
|
283
401
|
async fetchCode(url) {
|
|
284
402
|
const response = await fetch(url, {
|
|
@@ -306,25 +424,25 @@ class WuLoader {
|
|
|
306
424
|
* @param {Array} appConfigs - Configuraciones de aplicaciones
|
|
307
425
|
*/
|
|
308
426
|
async preload(appConfigs) {
|
|
309
|
-
logger.debug(`[WuLoader]
|
|
427
|
+
logger.debug(`[WuLoader] Preloading ${appConfigs.length} apps...`);
|
|
310
428
|
|
|
311
429
|
const preloadPromises = appConfigs.map(async (config) => {
|
|
312
430
|
try {
|
|
313
431
|
await this.loadApp(config.url, config.manifest);
|
|
314
|
-
logger.debug(`[WuLoader]
|
|
432
|
+
logger.debug(`[WuLoader] Preloaded: ${config.name}`);
|
|
315
433
|
} catch (error) {
|
|
316
|
-
logger.warn(`[WuLoader]
|
|
434
|
+
logger.warn(`[WuLoader] Failed to preload ${config.name}:`, error.message);
|
|
317
435
|
}
|
|
318
436
|
});
|
|
319
437
|
|
|
320
438
|
await Promise.allSettled(preloadPromises);
|
|
321
|
-
logger.debug(`[WuLoader]
|
|
439
|
+
logger.debug(`[WuLoader] Preload completed`);
|
|
322
440
|
}
|
|
323
441
|
|
|
324
442
|
/**
|
|
325
|
-
* Verificar si una URL
|
|
443
|
+
* Verificar si una URL esta disponible
|
|
326
444
|
* @param {string} url - URL a verificar
|
|
327
|
-
* @returns {boolean} True si
|
|
445
|
+
* @returns {boolean} True si esta disponible
|
|
328
446
|
*/
|
|
329
447
|
async isAvailable(url) {
|
|
330
448
|
try {
|
|
@@ -368,9 +486,9 @@ class WuLoader {
|
|
|
368
486
|
try {
|
|
369
487
|
const component = await this.loadComponent(app.url, exportPath);
|
|
370
488
|
resolved.set(importPath, component);
|
|
371
|
-
logger.debug(`[WuLoader]
|
|
489
|
+
logger.debug(`[WuLoader] Resolved dependency: ${importPath}`);
|
|
372
490
|
} catch (error) {
|
|
373
|
-
console.error(`[WuLoader]
|
|
491
|
+
console.error(`[WuLoader] Failed to resolve: ${importPath}`, error);
|
|
374
492
|
}
|
|
375
493
|
}
|
|
376
494
|
|
|
@@ -379,7 +497,7 @@ class WuLoader {
|
|
|
379
497
|
|
|
380
498
|
/**
|
|
381
499
|
* Limpiar cache
|
|
382
|
-
* @param {string} pattern -
|
|
500
|
+
* @param {string} pattern - Patron opcional para limpiar URLs especificas
|
|
383
501
|
*/
|
|
384
502
|
clearCache(pattern) {
|
|
385
503
|
if (pattern) {
|
|
@@ -387,21 +505,23 @@ class WuLoader {
|
|
|
387
505
|
for (const [url] of this.cache) {
|
|
388
506
|
if (regex.test(url)) {
|
|
389
507
|
this.cache.delete(url);
|
|
390
|
-
logger.debug(`[WuLoader]
|
|
508
|
+
logger.debug(`[WuLoader] Cleared cache for: ${url}`);
|
|
391
509
|
}
|
|
392
510
|
}
|
|
393
511
|
} else {
|
|
394
512
|
this.cache.clear();
|
|
395
|
-
logger.debug(`[WuLoader]
|
|
513
|
+
logger.debug(`[WuLoader] Cache cleared completely`);
|
|
396
514
|
}
|
|
397
515
|
}
|
|
398
516
|
|
|
399
517
|
/**
|
|
400
|
-
* Obtener
|
|
518
|
+
* Obtener estadisticas del loader
|
|
401
519
|
*/
|
|
402
520
|
getStats() {
|
|
403
521
|
return {
|
|
404
522
|
cached: this.cache.size,
|
|
523
|
+
maxCacheSize: this.maxCacheSize,
|
|
524
|
+
cacheTTL: this.cacheTTL,
|
|
405
525
|
loading: this.loadingPromises.size,
|
|
406
526
|
cacheKeys: Array.from(this.cache.keys())
|
|
407
527
|
};
|
|
@@ -899,8 +1019,9 @@ class WuStyleBridge {
|
|
|
899
1019
|
|
|
900
1020
|
|
|
901
1021
|
class WuProxySandbox {
|
|
902
|
-
constructor(appName) {
|
|
1022
|
+
constructor(appName, options = {}) {
|
|
903
1023
|
this.appName = appName;
|
|
1024
|
+
this.options = options;
|
|
904
1025
|
this.proxy = null;
|
|
905
1026
|
this.fakeWindow = Object.create(null);
|
|
906
1027
|
this.active = false;
|
|
@@ -2667,6 +2788,29 @@ class WuManifest {
|
|
|
2667
2788
|
}
|
|
2668
2789
|
}
|
|
2669
2790
|
|
|
2791
|
+
// Validate optional fields
|
|
2792
|
+
if (manifest.styleMode !== undefined) {
|
|
2793
|
+
const validModes = ['shared', 'isolated', 'fully-isolated'];
|
|
2794
|
+
if (!validModes.includes(manifest.styleMode)) {
|
|
2795
|
+
logger.warn(`[WuManifest] Invalid styleMode "${manifest.styleMode}", defaulting to "shared". Valid: ${validModes.join(', ')}`);
|
|
2796
|
+
manifest.styleMode = 'shared';
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
if (manifest.version !== undefined && typeof manifest.version !== 'string') {
|
|
2801
|
+
logger.warn('[WuManifest] version must be a string, ignoring');
|
|
2802
|
+
delete manifest.version;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
if (manifest.folder !== undefined) {
|
|
2806
|
+
if (typeof manifest.folder !== 'string') {
|
|
2807
|
+
logger.warn('[WuManifest] folder must be a string, ignoring');
|
|
2808
|
+
delete manifest.folder;
|
|
2809
|
+
} else if (this._hasDangerousPatterns(manifest.folder)) {
|
|
2810
|
+
throw new Error('folder contains dangerous patterns');
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2670
2814
|
// Normalizar y limpiar manifest
|
|
2671
2815
|
return this.normalize(manifest);
|
|
2672
2816
|
}
|
|
@@ -2884,6 +3028,15 @@ class WuManifest {
|
|
|
2884
3028
|
* - API minimalista: get(), set(), on()
|
|
2885
3029
|
*/
|
|
2886
3030
|
|
|
3031
|
+
/**
|
|
3032
|
+
* @typedef {Object} WuStoreMetrics
|
|
3033
|
+
* @property {number} reads - Total read operations
|
|
3034
|
+
* @property {number} writes - Total write operations
|
|
3035
|
+
* @property {number} notifications - Total notifications sent
|
|
3036
|
+
* @property {number} bufferUtilization - Ring buffer utilization (0-1)
|
|
3037
|
+
* @property {number} listenerCount - Active listener count
|
|
3038
|
+
*/
|
|
3039
|
+
|
|
2887
3040
|
class WuStore {
|
|
2888
3041
|
constructor(bufferSize = 256) {
|
|
2889
3042
|
// Ring Buffer configuration
|
|
@@ -2939,8 +3092,9 @@ class WuStore {
|
|
|
2939
3092
|
set(path, value) {
|
|
2940
3093
|
this.metrics.writes++;
|
|
2941
3094
|
|
|
2942
|
-
// Write to ring buffer (lock-free)
|
|
2943
|
-
const sequence = this.cursor
|
|
3095
|
+
// Write to ring buffer (lock-free, wraps at buffer boundary)
|
|
3096
|
+
const sequence = this.cursor;
|
|
3097
|
+
this.cursor = (this.cursor + 1) % (this.bufferSize * this.bufferSize);
|
|
2944
3098
|
const index = sequence & this.mask;
|
|
2945
3099
|
|
|
2946
3100
|
// Reuse buffer slot (zero allocation)
|
|
@@ -3027,7 +3181,7 @@ class WuStore {
|
|
|
3027
3181
|
getMetrics() {
|
|
3028
3182
|
return {
|
|
3029
3183
|
...this.metrics,
|
|
3030
|
-
bufferUtilization: (this.cursor
|
|
3184
|
+
bufferUtilization: Math.min(1, this.cursor / this.bufferSize),
|
|
3031
3185
|
listenerCount: this.listeners.size + this.patternListeners.size
|
|
3032
3186
|
};
|
|
3033
3187
|
}
|
|
@@ -3438,6 +3592,9 @@ class WuCache {
|
|
|
3438
3592
|
cooldownUntil: 0
|
|
3439
3593
|
};
|
|
3440
3594
|
|
|
3595
|
+
// Rate limit notification flag (log only once per cooldown)
|
|
3596
|
+
this._rateLimitNotified = false;
|
|
3597
|
+
|
|
3441
3598
|
// Memory cache
|
|
3442
3599
|
this.memoryCache = new Map();
|
|
3443
3600
|
|
|
@@ -3472,6 +3629,7 @@ class WuCache {
|
|
|
3472
3629
|
}
|
|
3473
3630
|
// Cooldown terminado
|
|
3474
3631
|
this.rateLimiting.inCooldown = false;
|
|
3632
|
+
this._rateLimitNotified = false;
|
|
3475
3633
|
this.rateLimiting.operations = [];
|
|
3476
3634
|
}
|
|
3477
3635
|
|
|
@@ -3497,7 +3655,22 @@ class WuCache {
|
|
|
3497
3655
|
}
|
|
3498
3656
|
|
|
3499
3657
|
/**
|
|
3500
|
-
*
|
|
3658
|
+
* Handle rate-limited operations: log once per cooldown, return diagnostic object
|
|
3659
|
+
* @param {string} operation - The operation that was rate limited ('get' or 'set')
|
|
3660
|
+
* @param {string} key - The cache key that was rejected
|
|
3661
|
+
* @returns {{ rateLimited: true, operation: string, key: string }}
|
|
3662
|
+
*/
|
|
3663
|
+
_onRateLimited(operation, key) {
|
|
3664
|
+
if (!this._rateLimitNotified) {
|
|
3665
|
+
const cooldownRemaining = Math.max(0, this.rateLimiting.cooldownUntil - Date.now());
|
|
3666
|
+
logger.warn(`[WuCache] Rate limited: ${operation} for key "${key}" rejected. ${cooldownRemaining}ms remaining in cooldown.`);
|
|
3667
|
+
this._rateLimitNotified = true;
|
|
3668
|
+
}
|
|
3669
|
+
return { rateLimited: true, operation, key };
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
/**
|
|
3673
|
+
* GET RATE LIMIT STATUS
|
|
3501
3674
|
*/
|
|
3502
3675
|
getRateLimitStatus() {
|
|
3503
3676
|
const now = Date.now();
|
|
@@ -3521,7 +3694,8 @@ class WuCache {
|
|
|
3521
3694
|
get(key) {
|
|
3522
3695
|
// 🔐 Check rate limit
|
|
3523
3696
|
if (!this._checkRateLimit()) {
|
|
3524
|
-
|
|
3697
|
+
this._onRateLimited('get', key);
|
|
3698
|
+
return null;
|
|
3525
3699
|
}
|
|
3526
3700
|
|
|
3527
3701
|
// 1. Buscar en memoria
|
|
@@ -3568,7 +3742,8 @@ class WuCache {
|
|
|
3568
3742
|
set(key, value, ttl) {
|
|
3569
3743
|
// 🔐 Check rate limit
|
|
3570
3744
|
if (!this._checkRateLimit()) {
|
|
3571
|
-
|
|
3745
|
+
this._onRateLimited('set', key);
|
|
3746
|
+
return false;
|
|
3572
3747
|
}
|
|
3573
3748
|
|
|
3574
3749
|
try {
|
|
@@ -3893,6 +4068,25 @@ class WuCache {
|
|
|
3893
4068
|
* - Verificación de apps autorizadas
|
|
3894
4069
|
*/
|
|
3895
4070
|
|
|
4071
|
+
/**
|
|
4072
|
+
* @typedef {Object} WuEvent
|
|
4073
|
+
* @property {string} name - Event name
|
|
4074
|
+
* @property {*} data - Event payload
|
|
4075
|
+
* @property {number} timestamp - Event timestamp
|
|
4076
|
+
* @property {string} appName - Source app name
|
|
4077
|
+
* @property {Object} meta - Additional metadata
|
|
4078
|
+
* @property {boolean} verified - Whether origin was verified
|
|
4079
|
+
*/
|
|
4080
|
+
|
|
4081
|
+
/**
|
|
4082
|
+
* @typedef {Object} WuEventBusConfig
|
|
4083
|
+
* @property {number} [maxHistory=100] - Maximum events in history
|
|
4084
|
+
* @property {boolean} [enableReplay=true] - Enable event replay
|
|
4085
|
+
* @property {boolean} [enableWildcards=true] - Enable wildcard matching
|
|
4086
|
+
* @property {boolean} [logEvents=false] - Log all events
|
|
4087
|
+
* @property {boolean} [strictMode=false] - Reject unauthorized events
|
|
4088
|
+
* @property {boolean} [validateOrigin=true] - Validate event origins
|
|
4089
|
+
*/
|
|
3896
4090
|
|
|
3897
4091
|
class WuEventBus {
|
|
3898
4092
|
constructor() {
|
|
@@ -3903,16 +4097,21 @@ class WuEventBus {
|
|
|
3903
4097
|
this.authorizedApps = new Map(); // appName -> { token, permissions }
|
|
3904
4098
|
this.trustedEvents = new Set(['wu:*', 'system:*']); // Eventos del sistema
|
|
3905
4099
|
|
|
4100
|
+
// Auto-detect production environment for strictMode default
|
|
4101
|
+
const isProduction = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production';
|
|
4102
|
+
|
|
3906
4103
|
this.config = {
|
|
3907
4104
|
maxHistory: 100,
|
|
3908
4105
|
enableReplay: true,
|
|
3909
4106
|
enableWildcards: true,
|
|
3910
4107
|
logEvents: false,
|
|
3911
4108
|
// 🔐 Opciones de seguridad
|
|
3912
|
-
strictMode:
|
|
4109
|
+
strictMode: isProduction, // Auto-enabled in production, permissive in development
|
|
3913
4110
|
validateOrigin: true // Valida que appName sea una app registrada
|
|
3914
4111
|
};
|
|
3915
4112
|
|
|
4113
|
+
this._permissiveWarned = false;
|
|
4114
|
+
|
|
3916
4115
|
this.stats = {
|
|
3917
4116
|
emitted: 0,
|
|
3918
4117
|
subscriptions: 0,
|
|
@@ -4010,6 +4209,19 @@ class WuEventBus {
|
|
|
4010
4209
|
return `wu_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
4011
4210
|
}
|
|
4012
4211
|
|
|
4212
|
+
/**
|
|
4213
|
+
* WARN PERMISSIVE MODE: Log a one-time warning when strictMode is off
|
|
4214
|
+
* Alerts developers that events are flowing without authorization checks
|
|
4215
|
+
*/
|
|
4216
|
+
_warnPermissiveMode() {
|
|
4217
|
+
if (this._permissiveWarned) return;
|
|
4218
|
+
this._permissiveWarned = true;
|
|
4219
|
+
logger.warn(
|
|
4220
|
+
'[WuEventBus] strictMode is disabled. Events are emitted without authorization checks. ' +
|
|
4221
|
+
'Enable strictMode for production by calling enableStrictMode() or setting NODE_ENV=production.'
|
|
4222
|
+
);
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4013
4225
|
/**
|
|
4014
4226
|
* 📢 EMIT: Emitir evento con validación de origen
|
|
4015
4227
|
* @param {string} eventName - Nombre del evento
|
|
@@ -4019,6 +4231,11 @@ class WuEventBus {
|
|
|
4019
4231
|
emit(eventName, data, options = {}) {
|
|
4020
4232
|
const appName = options.appName || 'unknown';
|
|
4021
4233
|
|
|
4234
|
+
// Warn once if running in permissive mode (strictMode off)
|
|
4235
|
+
if (!this.config.strictMode) {
|
|
4236
|
+
this._warnPermissiveMode();
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4022
4239
|
// 🔐 Validar origen si está habilitado
|
|
4023
4240
|
if (this.config.validateOrigin && this.config.strictMode) {
|
|
4024
4241
|
if (!this._validateOrigin(eventName, appName, options.token)) {
|
|
@@ -4470,7 +4687,7 @@ class WuPerformance {
|
|
|
4470
4687
|
|
|
4471
4688
|
|
|
4472
4689
|
class WuPluginSystem {
|
|
4473
|
-
constructor(core) {
|
|
4690
|
+
constructor(core, options = {}) {
|
|
4474
4691
|
this._core = core; // Privado - no expuesto a plugins
|
|
4475
4692
|
this.plugins = new Map();
|
|
4476
4693
|
this.hooks = new Map();
|
|
@@ -4494,7 +4711,7 @@ class WuPluginSystem {
|
|
|
4494
4711
|
];
|
|
4495
4712
|
|
|
4496
4713
|
// 🔐 Timeout para hooks (evita que plugins bloqueen)
|
|
4497
|
-
this.hookTimeout = 5000; // 5 segundos
|
|
4714
|
+
this.hookTimeout = options.hookTimeout || 5000; // Default: 5 segundos
|
|
4498
4715
|
|
|
4499
4716
|
this.availableHooks.forEach(hook => {
|
|
4500
4717
|
this.hooks.set(hook, []);
|
|
@@ -4565,8 +4782,61 @@ class WuPluginSystem {
|
|
|
4565
4782
|
logger.warn('[WuPlugin] ⚠️ Plugin has unsafe access to core!');
|
|
4566
4783
|
}
|
|
4567
4784
|
|
|
4568
|
-
// Congelar API para evitar modificaciones
|
|
4569
|
-
return
|
|
4785
|
+
// Congelar API recursivamente para evitar modificaciones en cualquier nivel
|
|
4786
|
+
return WuPluginSystem._deepFreeze(api);
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4789
|
+
/**
|
|
4790
|
+
* Deep-freeze an object and all its nested plain objects and arrays.
|
|
4791
|
+
* Functions, DOM nodes, and other non-plain types are left untouched
|
|
4792
|
+
* so they remain callable / functional. A WeakSet guards against
|
|
4793
|
+
* circular references.
|
|
4794
|
+
*
|
|
4795
|
+
* @param {*} obj - The value to deep-freeze
|
|
4796
|
+
* @param {WeakSet} [seen] - Internal tracker for circular references
|
|
4797
|
+
* @returns {*} The same object, now deeply frozen
|
|
4798
|
+
*/
|
|
4799
|
+
static _deepFreeze(obj, seen) {
|
|
4800
|
+
if (obj === null || obj === undefined) {
|
|
4801
|
+
return obj;
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4804
|
+
// Only freeze plain objects and arrays.
|
|
4805
|
+
// Functions must stay invocable; DOM nodes, class instances, etc.
|
|
4806
|
+
// should not be tampered with.
|
|
4807
|
+
const dominated = typeof obj === 'object';
|
|
4808
|
+
if (!dominated) {
|
|
4809
|
+
return obj;
|
|
4810
|
+
}
|
|
4811
|
+
|
|
4812
|
+
const isPlainObject =
|
|
4813
|
+
Object.getPrototypeOf(obj) === Object.prototype ||
|
|
4814
|
+
Object.getPrototypeOf(obj) === null;
|
|
4815
|
+
const isArray = Array.isArray(obj);
|
|
4816
|
+
|
|
4817
|
+
if (!isPlainObject && !isArray) {
|
|
4818
|
+
return obj;
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
// Circular-reference guard
|
|
4822
|
+
if (!seen) {
|
|
4823
|
+
seen = new WeakSet();
|
|
4824
|
+
}
|
|
4825
|
+
if (seen.has(obj)) {
|
|
4826
|
+
return obj;
|
|
4827
|
+
}
|
|
4828
|
+
seen.add(obj);
|
|
4829
|
+
|
|
4830
|
+
// Recurse into own enumerable properties
|
|
4831
|
+
const keys = Object.keys(obj);
|
|
4832
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4833
|
+
const value = obj[keys[i]];
|
|
4834
|
+
if (value !== null && value !== undefined && typeof value === 'object') {
|
|
4835
|
+
WuPluginSystem._deepFreeze(value, seen);
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
|
|
4839
|
+
return Object.freeze(obj);
|
|
4570
4840
|
}
|
|
4571
4841
|
|
|
4572
4842
|
/**
|
|
@@ -5377,19 +5647,33 @@ class WuErrorBoundary {
|
|
|
5377
5647
|
* @param {Object} context - Contexto
|
|
5378
5648
|
*/
|
|
5379
5649
|
logError(error, context) {
|
|
5650
|
+
// Truncate stack to first 5 lines to prevent retaining large object references
|
|
5651
|
+
const stack = error.stack ? error.stack.split('\n').slice(0, 5).join('\n') : '';
|
|
5652
|
+
|
|
5653
|
+
// Shallow-copy context to avoid retaining references to live objects
|
|
5654
|
+
const safeContext = {};
|
|
5655
|
+
for (const key of Object.keys(context)) {
|
|
5656
|
+
const val = context[key];
|
|
5657
|
+
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean' || val === null) {
|
|
5658
|
+
safeContext[key] = val;
|
|
5659
|
+
} else {
|
|
5660
|
+
safeContext[key] = String(val);
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
|
|
5380
5664
|
const errorEntry = {
|
|
5381
5665
|
error: {
|
|
5382
5666
|
name: error.name,
|
|
5383
5667
|
message: error.message,
|
|
5384
|
-
stack
|
|
5668
|
+
stack
|
|
5385
5669
|
},
|
|
5386
|
-
context,
|
|
5670
|
+
context: safeContext,
|
|
5387
5671
|
timestamp: Date.now()
|
|
5388
5672
|
};
|
|
5389
5673
|
|
|
5390
5674
|
this.errorLog.push(errorEntry);
|
|
5391
5675
|
|
|
5392
|
-
//
|
|
5676
|
+
// Maintain log limit
|
|
5393
5677
|
if (this.errorLog.length > this.maxErrorLog) {
|
|
5394
5678
|
this.errorLog.shift();
|
|
5395
5679
|
}
|
|
@@ -5857,19 +6141,28 @@ class WuHtmlParser {
|
|
|
5857
6141
|
return this._cache.get(cacheKey);
|
|
5858
6142
|
}
|
|
5859
6143
|
|
|
5860
|
-
const
|
|
5861
|
-
|
|
6144
|
+
const parser = new DOMParser();
|
|
6145
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
5862
6146
|
|
|
5863
6147
|
const inlineScripts = [];
|
|
5864
6148
|
const externalScripts = [];
|
|
5865
6149
|
const inlineStyles = [];
|
|
5866
6150
|
const externalStyles = [];
|
|
5867
6151
|
|
|
5868
|
-
|
|
6152
|
+
const ctx = {
|
|
5869
6153
|
inlineScripts, externalScripts,
|
|
5870
6154
|
inlineStyles, externalStyles,
|
|
5871
6155
|
baseUrl
|
|
5872
|
-
}
|
|
6156
|
+
};
|
|
6157
|
+
|
|
6158
|
+
// DOMParser moves <style>, <link>, and some <script> tags to <head>.
|
|
6159
|
+
// Extract resources from both head and body to capture everything.
|
|
6160
|
+
if (doc.head) {
|
|
6161
|
+
this._extractResources(doc.head, ctx);
|
|
6162
|
+
}
|
|
6163
|
+
|
|
6164
|
+
const temp = doc.body || doc.documentElement;
|
|
6165
|
+
this._extractResources(temp, ctx);
|
|
5873
6166
|
|
|
5874
6167
|
const result = {
|
|
5875
6168
|
dom: temp.innerHTML,
|
|
@@ -6000,6 +6293,52 @@ class WuHtmlParser {
|
|
|
6000
6293
|
|
|
6001
6294
|
class WuScriptExecutor {
|
|
6002
6295
|
|
|
6296
|
+
/**
|
|
6297
|
+
* Dangerous patterns that indicate prototype pollution, sandbox escape,
|
|
6298
|
+
* or direct access to sensitive APIs. Each entry is a regex paired with
|
|
6299
|
+
* a human-readable label used in error messages.
|
|
6300
|
+
*
|
|
6301
|
+
* This is a tripwire, not a full parser. It catches the most common
|
|
6302
|
+
* attack vectors without the overhead of AST analysis.
|
|
6303
|
+
*/
|
|
6304
|
+
static DANGEROUS_PATTERNS = [
|
|
6305
|
+
// Prototype pollution vectors
|
|
6306
|
+
{ pattern: /constructor\s*\[\s*['"`]constructor['"`]\s*\]/, label: 'constructor chain access (sandbox escape)' },
|
|
6307
|
+
{ pattern: /__proto__/, label: '__proto__ access (prototype pollution)' },
|
|
6308
|
+
|
|
6309
|
+
// Sandbox escape via proxy introspection
|
|
6310
|
+
{ pattern: /Object\s*\.\s*getPrototypeOf\s*\(\s*proxy\s*\)/, label: 'Object.getPrototypeOf(proxy) (sandbox escape)' },
|
|
6311
|
+
|
|
6312
|
+
// Dynamic code generation that bypasses the sandbox
|
|
6313
|
+
{ pattern: /Function\s*\(\s*['"`]/, label: 'Function() constructor (dynamic code generation)' },
|
|
6314
|
+
{ pattern: /\beval\s*\(/, label: 'eval() (dynamic code execution)' },
|
|
6315
|
+
|
|
6316
|
+
// Dynamic import escapes the sandbox entirely (runs in global scope)
|
|
6317
|
+
{ pattern: /\bimport\s*\(/, label: 'import() (dynamic import escapes sandbox)' },
|
|
6318
|
+
|
|
6319
|
+
// Direct cookie access (should go through proxy traps, not raw document)
|
|
6320
|
+
{ pattern: /document\s*\.\s*cookie/, label: 'document.cookie (direct cookie access)' },
|
|
6321
|
+
];
|
|
6322
|
+
|
|
6323
|
+
/**
|
|
6324
|
+
* Validate script text against known dangerous patterns before execution.
|
|
6325
|
+
* Throws if any pattern matches. This is intentionally lightweight --
|
|
6326
|
+
* pattern detection only, not a full parse.
|
|
6327
|
+
*
|
|
6328
|
+
* @param {string} scriptText - The raw script to validate
|
|
6329
|
+
* @param {string} appName - App identifier (for error context)
|
|
6330
|
+
* @throws {Error} If a dangerous pattern is detected
|
|
6331
|
+
*/
|
|
6332
|
+
_validateScript(scriptText, appName) {
|
|
6333
|
+
for (const { pattern, label } of WuScriptExecutor.DANGEROUS_PATTERNS) {
|
|
6334
|
+
if (pattern.test(scriptText)) {
|
|
6335
|
+
const msg = `[ScriptExecutor] Blocked dangerous pattern in "${appName}": ${label}`;
|
|
6336
|
+
logger.wuError(msg);
|
|
6337
|
+
throw new Error(msg);
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
}
|
|
6341
|
+
|
|
6003
6342
|
/**
|
|
6004
6343
|
* Execute a script string inside the proxy sandbox.
|
|
6005
6344
|
*
|
|
@@ -6016,6 +6355,8 @@ class WuScriptExecutor {
|
|
|
6016
6355
|
|
|
6017
6356
|
if (!scriptText || !scriptText.trim()) return;
|
|
6018
6357
|
|
|
6358
|
+
this._validateScript(scriptText, appName);
|
|
6359
|
+
|
|
6019
6360
|
const sourceComment = sourceUrl ? `\n//# sourceURL=wu-sandbox:///${appName}/${sourceUrl}\n` : '';
|
|
6020
6361
|
|
|
6021
6362
|
let wrappedCode;
|
|
@@ -7666,6 +8007,20 @@ class WuCore {
|
|
|
7666
8007
|
} catch (error) {
|
|
7667
8008
|
logger.wuError(`Mount attempt ${attempt + 1} failed for ${appName}:`, error);
|
|
7668
8009
|
|
|
8010
|
+
// Cleanup sandbox to prevent orphaned shadow DOMs
|
|
8011
|
+
try {
|
|
8012
|
+
if (this.sandbox && this.sandbox.sandboxes && this.sandbox.sandboxes.has(appName)) {
|
|
8013
|
+
const sb = this.sandbox.sandboxes.get(appName);
|
|
8014
|
+
if (sb && sb.proxySandbox) {
|
|
8015
|
+
sb.proxySandbox.deactivate();
|
|
8016
|
+
}
|
|
8017
|
+
this.sandbox.sandboxes.delete(appName);
|
|
8018
|
+
logger.wuDebug(`Sandbox cleaned up after mount failure for ${appName}`);
|
|
8019
|
+
}
|
|
8020
|
+
} catch (cleanupError) {
|
|
8021
|
+
logger.wuWarn(`Sandbox cleanup failed for ${appName}:`, cleanupError);
|
|
8022
|
+
}
|
|
8023
|
+
|
|
7669
8024
|
// Use error boundary for intelligent error handling
|
|
7670
8025
|
const errorResult = await this.errorBoundary.handle(error, {
|
|
7671
8026
|
appName,
|
|
@@ -15172,10 +15527,20 @@ if (typeof window !== 'undefined' && window.wu && window.wu._isWuFramework) {
|
|
|
15172
15527
|
} else {
|
|
15173
15528
|
wu = new WuCore();
|
|
15174
15529
|
wu._isWuFramework = true;
|
|
15530
|
+
|
|
15531
|
+
// Also store on a Symbol key for collision-safe access
|
|
15532
|
+
if (typeof window !== 'undefined') {
|
|
15533
|
+
const WU_KEY = Symbol.for('wu-framework');
|
|
15534
|
+
window[WU_KEY] = wu;
|
|
15535
|
+
}
|
|
15175
15536
|
}
|
|
15176
15537
|
|
|
15177
15538
|
// Expose globally for microfrontends
|
|
15178
15539
|
if (typeof window !== 'undefined') {
|
|
15540
|
+
// Warn if window.wu exists but is not a Wu Framework instance
|
|
15541
|
+
if (window.wu && !window.wu._isWuFramework) {
|
|
15542
|
+
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.');
|
|
15543
|
+
}
|
|
15179
15544
|
window.wu = wu;
|
|
15180
15545
|
|
|
15181
15546
|
if (!wu.version) {
|