wu-framework 1.1.1 → 1.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wu-framework",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "🚀 Universal Microfrontends Framework - 8 frameworks, zero config, Shadow DOM isolation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -587,6 +587,23 @@ export class WuCore {
587
587
  // 🏊 Acquire sandbox from pool (if configured)
588
588
  const poolSandbox = this.sandboxPool.acquire(appName);
589
589
 
590
+ // 🎨 Configure style mode from manifest before creating sandbox
591
+ const manifest = app.manifest;
592
+ console.log(`[Wu] 🔍 DEBUG: Manifest for ${appName}:`, JSON.stringify(manifest, null, 2));
593
+
594
+ // Default to 'isolated' if no styleMode specified
595
+ const styleMode = manifest?.styleMode || 'isolated';
596
+ if (this.sandbox.styleBridge) {
597
+ // Pass app URL so StyleBridge can identify MFE's own styles
598
+ this.sandbox.styleBridge.setAppStyleMode(appName, styleMode, app.url);
599
+ console.log(`[Wu] 🎨 Style mode for ${appName}: ${styleMode}`);
600
+
601
+ // 📁 If folder is specified in manifest, register it for style detection
602
+ // This is needed when folder name differs from app name (e.g., folder="container", name="content")
603
+ const folderName = manifest?.folder || appName;
604
+ this.sandbox.styleBridge.setAppFolder(appName, `/${folderName}/`);
605
+ }
606
+
590
607
  // 🛡️ Quantum sandbox creation
591
608
  const sandbox = this.sandbox.create(appName, container);
592
609
 
@@ -603,6 +620,14 @@ export class WuCore {
603
620
  if (!lifecycle) {
604
621
  throw new Error(`App ${appName} did not register with wu.define()`);
605
622
  }
623
+
624
+ // 🎨 RE-INJECT STYLES: After remote module loads, Vite may have injected new styles
625
+ // We need to re-scan and inject any new styles into the Shadow DOM
626
+ if (sandbox.shadowRoot && this.sandbox.styleBridge) {
627
+ console.log(`[Wu] 🎨 Re-injecting styles after module load for ${appName}...`);
628
+ // Pass app URL so StyleBridge can identify MFE's own styles
629
+ await this.sandbox.styleBridge.injectStylesIntoShadow(sandbox.shadowRoot, appName, app.url);
630
+ }
606
631
  }
607
632
 
608
633
  // 🪝 Execute beforeMount hooks
@@ -43,14 +43,19 @@ export class WuManifest {
43
43
  defineSchema() {
44
44
  this.schemas.set('wu.json', {
45
45
  required: ['name', 'entry'],
46
- optional: ['wu'],
46
+ optional: ['wu', 'styleMode'],
47
47
  wu: {
48
48
  optional: ['exports', 'imports', 'routes', 'permissions'],
49
49
  exports: 'object',
50
50
  imports: 'array',
51
51
  routes: 'array',
52
52
  permissions: 'array'
53
- }
53
+ },
54
+ // 🎨 styleMode: 'isolated' | 'shared' | 'auto'
55
+ // - isolated: No comparte estilos del padre (default, máximo aislamiento)
56
+ // - shared: Comparte todos los estilos del padre
57
+ // - auto: Solo comparte estilos de librerías específicas (element-plus, etc.)
58
+ styleMode: 'string'
54
59
  });
55
60
  }
56
61
 
@@ -312,7 +317,11 @@ export class WuManifest {
312
317
  normalize(manifest) {
313
318
  const normalized = {
314
319
  name: manifest.name.trim(),
320
+ // 📁 folder: carpeta física del MFE (si difiere del name)
321
+ folder: manifest.folder?.trim() || manifest.name.trim(),
315
322
  entry: this.normalizeEntry(manifest.entry),
323
+ // 🎨 styleMode: 'isolated' (default) | 'shared' | 'auto'
324
+ styleMode: this.normalizeStyleMode(manifest.styleMode),
316
325
  wu: {
317
326
  exports: manifest.wu?.exports || {},
318
327
  imports: manifest.wu?.imports || [],
@@ -342,6 +351,26 @@ export class WuManifest {
342
351
  return normalized;
343
352
  }
344
353
 
354
+ /**
355
+ * 🎨 Normalizar styleMode
356
+ * @param {string} styleMode - Modo de estilos
357
+ * @returns {'isolated' | 'fully-isolated' | 'shared' | 'auto'} styleMode normalizado
358
+ *
359
+ * Modos disponibles:
360
+ * - 'isolated': MFE solo tiene sus propios estilos (CSS variables SÍ se heredan)
361
+ * - 'fully-isolated': Aislamiento TOTAL - resetea CSS variables del padre (ÚNICO en wu-framework!)
362
+ * - 'shared': Comparte todos los estilos del padre
363
+ * - 'auto': Comparte solo librerías específicas (element-plus, etc.)
364
+ */
365
+ normalizeStyleMode(styleMode) {
366
+ const validModes = ['isolated', 'fully-isolated', 'shared', 'auto'];
367
+ if (styleMode && validModes.includes(styleMode)) {
368
+ return styleMode;
369
+ }
370
+ // Default: isolated (máximo aislamiento sin romper CSS variables)
371
+ return 'isolated';
372
+ }
373
+
345
374
  /**
346
375
  * Normalizar entry path
347
376
  * @param {string} entry - Entry path
@@ -3,14 +3,26 @@
3
3
  *
4
4
  * Comparte automáticamente estilos de node_modules entre padre e hijos Shadow DOM
5
5
  * Soluciona el problema de aislamiento CSS en microfrontends
6
+ *
7
+ * 🚀 STYLE MODES:
8
+ * - 'isolated': MFE solo tiene sus propios estilos (CSS variables SÍ se heredan)
9
+ * - 'fully-isolated': Aislamiento TOTAL - resetea CSS variables del padre
10
+ * - 'shared': Comparte todos los estilos del padre
11
+ * - 'auto': Comparte solo librerías específicas (element-plus, etc.)
6
12
  */
7
13
 
8
14
  export class WuStyleBridge {
9
15
  constructor() {
10
16
  this.sharedStyles = new Map();
11
17
  this.styleObserver = null;
18
+ this.appStyleModes = new Map(); // Per-app style isolation mode
12
19
  this.config = {
13
- // Librerías que se deben compartir automáticamente
20
+ // 🛡️ DEFAULT MODE: 'isolated' = cada MFE tiene sus propios estilos
21
+ // 'fully-isolated' = aislamiento total incluyendo CSS variables
22
+ // 'shared' = compartir estilos del padre
23
+ // 'auto' = compartir solo librerías específicas
24
+ defaultMode: 'isolated',
25
+ // Librerías que se deben compartir automáticamente (solo en modo 'auto')
14
26
  autoShareLibraries: [
15
27
  'element-plus',
16
28
  'vue-flow',
@@ -20,20 +32,51 @@ export class WuStyleBridge {
20
32
  'normalize.css',
21
33
  'reset.css'
22
34
  ],
23
- // Patrones de URLs a compartir
35
+ // Patrones de URLs a compartir (solo en modo 'auto' o 'shared')
24
36
  sharePatterns: [
25
37
  /\/node_modules\//,
26
38
  /\/@vite\/client/,
27
39
  /\/dist\/index\.css$/,
28
40
  /\/dist\/style\.css$/
29
41
  ],
30
- // Modo de compartición
31
- mode: 'auto', // 'auto' | 'manual' | 'all'
32
42
  // Caché de estilos
33
43
  cacheEnabled: true
34
44
  };
35
45
 
36
- console.log('[WuStyleBridge] 🎨 Style sharing system initialized');
46
+ console.log('[WuStyleBridge] 🎨 Style sharing system initialized (default: isolated)');
47
+ }
48
+
49
+ /**
50
+ * 🛡️ SET APP STYLE MODE: Configura el modo de estilos para una app específica
51
+ * @param {string} appName - Nombre de la app
52
+ * @param {'isolated' | 'shared' | 'auto'} mode - Modo de estilos
53
+ * @param {string} appUrl - URL base del MFE (opcional)
54
+ */
55
+ setAppStyleMode(appName, mode, appUrl = null) {
56
+ this.appStyleModes.set(appName, mode);
57
+ if (appUrl) {
58
+ this.appUrls = this.appUrls || new Map();
59
+ this.appUrls.set(appName, appUrl);
60
+ }
61
+ console.log(`[WuStyleBridge] 🎨 Style mode for ${appName}: ${mode}${appUrl ? ` (url: ${appUrl})` : ''}`);
62
+ }
63
+
64
+ /**
65
+ * 🔗 GET APP URL: Obtiene la URL de una app
66
+ * @param {string} appName - Nombre de la app
67
+ * @returns {string|null}
68
+ */
69
+ getAppUrl(appName) {
70
+ return this.appUrls?.get(appName) || null;
71
+ }
72
+
73
+ /**
74
+ * 🔍 GET APP STYLE MODE: Obtiene el modo de estilos de una app
75
+ * @param {string} appName - Nombre de la app
76
+ * @returns {'isolated' | 'shared' | 'auto'}
77
+ */
78
+ getAppStyleMode(appName) {
79
+ return this.appStyleModes.get(appName) || this.config.defaultMode;
37
80
  }
38
81
 
39
82
  /**
@@ -137,26 +180,61 @@ export class WuStyleBridge {
137
180
  }
138
181
 
139
182
  /**
140
- * 🌉 INYECTAR ESTILOS EN SHADOW DOM: Clona estilos al Shadow DOM
183
+ * 🌉 INYECTAR ESTILOS EN SHADOW DOM: Clona estilos al Shadow DOM según el modo
141
184
  * @param {ShadowRoot} shadowRoot - Shadow DOM donde inyectar
142
185
  * @param {string} appName - Nombre de la app
186
+ * @param {string} appUrl - URL base del MFE (opcional, para detectar sus propios estilos)
143
187
  * @returns {Promise<number>}
144
188
  */
145
- async injectStylesIntoShadow(shadowRoot, appName) {
189
+ async injectStylesIntoShadow(shadowRoot, appName, appUrl = null) {
146
190
  if (!shadowRoot) {
147
191
  console.warn('[WuStyleBridge] ⚠️ No shadow root provided');
148
192
  return 0;
149
193
  }
150
194
 
151
- console.log(`[WuStyleBridge] 🌉 Injecting shared styles into ${appName}...`);
195
+ // 🛡️ CHECK STYLE MODE
196
+ const styleMode = this.getAppStyleMode(appName);
197
+ // Use stored URL if not provided
198
+ const resolvedAppUrl = appUrl || this.getAppUrl(appName);
199
+ console.log(`[WuStyleBridge] 🌉 Injecting styles into ${appName} (mode: ${styleMode}, url: ${resolvedAppUrl || 'none'})...`);
200
+
201
+ // 🔒 FULLY-ISOLATED MODE: Inject CSS variables reset FIRST
202
+ if (styleMode === 'fully-isolated') {
203
+ this.injectCSSVariablesReset(shadowRoot, appName);
204
+ }
152
205
 
153
206
  // Detectar estilos del documento
154
207
  const styles = this.detectDocumentStyles();
155
208
  let injectedCount = 0;
156
209
 
157
- // Inyectar cada estilo
210
+ // Inyectar cada estilo según el modo
158
211
  for (const style of styles) {
159
212
  try {
213
+ const styleSource = style.href || style.viteId || '';
214
+
215
+ // 🎯 DETECTAR SI EL ESTILO PERTENECE AL MFE
216
+ const isOwnStyle = this.isStyleFromApp(styleSource, appName, resolvedAppUrl);
217
+
218
+ // 🛡️ En modo 'isolated' o 'fully-isolated': SOLO inyectar estilos propios del MFE
219
+ if (styleMode === 'isolated' || styleMode === 'fully-isolated') {
220
+ if (!isOwnStyle) {
221
+ const shortSource = styleSource.split(/[/\\]/).slice(-2).join('/');
222
+ console.log(`[WuStyleBridge] 🚫 ${appName} skipping foreign style: ${shortSource}`);
223
+ continue; // Skip parent/shell styles
224
+ }
225
+ console.log(`[WuStyleBridge] 🎨 ${appName} own style: ${styleSource.split(/[/\\]/).slice(-2).join('/')}`);
226
+ }
227
+
228
+ // 🎯 En modo 'auto': compartir librerías + estilos propios
229
+ if (styleMode === 'auto') {
230
+ const shouldShare = this.shouldShareStyle(styleSource);
231
+ if (!shouldShare && !isOwnStyle) {
232
+ continue; // Skip this style
233
+ }
234
+ }
235
+
236
+ // 'shared' mode: inject everything
237
+
160
238
  switch (style.type) {
161
239
  case 'link':
162
240
  await this.injectLinkStyle(shadowRoot, style);
@@ -182,6 +260,242 @@ export class WuStyleBridge {
182
260
  return injectedCount;
183
261
  }
184
262
 
263
+ /**
264
+ * 🔒 INJECT CSS VARIABLES RESET: Resetea las CSS variables heredadas del padre
265
+ * Este método detecta todas las CSS variables definidas en el documento padre
266
+ * y las resetea a 'initial' dentro del Shadow DOM para lograr aislamiento total.
267
+ *
268
+ * @param {ShadowRoot} shadowRoot - Shadow DOM donde inyectar el reset
269
+ * @param {string} appName - Nombre de la app para logging
270
+ */
271
+ injectCSSVariablesReset(shadowRoot, appName) {
272
+ // Verificar si ya existe el reset
273
+ const existingReset = shadowRoot.querySelector('style[data-wu-css-reset="true"]');
274
+ if (existingReset) {
275
+ console.log(`[WuStyleBridge] ⏭️ CSS variables reset already exists for ${appName}`);
276
+ return;
277
+ }
278
+
279
+ // 🔍 Detectar todas las CSS variables del documento padre
280
+ const parentVariables = this.detectParentCSSVariables();
281
+
282
+ if (parentVariables.length === 0) {
283
+ console.log(`[WuStyleBridge] 🔒 No parent CSS variables to reset for ${appName}`);
284
+ return;
285
+ }
286
+
287
+ // 🎨 Generar CSS reset
288
+ const resetCSS = this.generateCSSVariablesReset(parentVariables);
289
+
290
+ // Crear style tag con el reset
291
+ const resetStyle = document.createElement('style');
292
+ resetStyle.setAttribute('data-wu-css-reset', 'true');
293
+ resetStyle.setAttribute('data-wu-app', appName);
294
+ resetStyle.textContent = resetCSS;
295
+
296
+ // Insertar al PRINCIPIO del shadow root (antes de otros estilos)
297
+ shadowRoot.insertBefore(resetStyle, shadowRoot.firstChild);
298
+
299
+ console.log(`[WuStyleBridge] 🔒 CSS variables reset injected for ${appName} (${parentVariables.length} variables blocked)`);
300
+ }
301
+
302
+ /**
303
+ * 🔍 DETECT PARENT CSS VARIABLES: Detecta todas las CSS variables del documento padre
304
+ * @returns {Array<{name: string, value: string}>} Lista de variables detectadas
305
+ */
306
+ detectParentCSSVariables() {
307
+ const variables = [];
308
+ const seen = new Set();
309
+
310
+ try {
311
+ // 1. Buscar en :root y html
312
+ const rootStyles = getComputedStyle(document.documentElement);
313
+
314
+ // 2. Buscar en todos los stylesheets del documento
315
+ for (const sheet of document.styleSheets) {
316
+ try {
317
+ // Algunos stylesheets (cross-origin) no permiten acceso
318
+ const rules = sheet.cssRules || sheet.rules;
319
+ if (!rules) continue;
320
+
321
+ for (const rule of rules) {
322
+ // Solo procesar reglas de estilo
323
+ if (rule.type !== CSSRule.STYLE_RULE) continue;
324
+
325
+ // Buscar reglas que definen variables (:root, html, body, *)
326
+ const selector = rule.selectorText?.toLowerCase() || '';
327
+ if (selector === ':root' || selector === 'html' || selector === 'body' || selector === '*') {
328
+ const style = rule.style;
329
+ for (let i = 0; i < style.length; i++) {
330
+ const prop = style[i];
331
+ // Las CSS variables empiezan con --
332
+ if (prop.startsWith('--') && !seen.has(prop)) {
333
+ seen.add(prop);
334
+ variables.push({
335
+ name: prop,
336
+ value: style.getPropertyValue(prop).trim()
337
+ });
338
+ }
339
+ }
340
+ }
341
+ }
342
+ } catch (e) {
343
+ // Ignorar errores de CORS en stylesheets externos
344
+ }
345
+ }
346
+
347
+ // 3. También detectar variables inline en el html element
348
+ const htmlStyle = document.documentElement.getAttribute('style') || '';
349
+ const inlineVarMatches = htmlStyle.matchAll(/--([\w-]+)\s*:\s*([^;]+)/g);
350
+ for (const match of inlineVarMatches) {
351
+ const name = `--${match[1]}`;
352
+ if (!seen.has(name)) {
353
+ seen.add(name);
354
+ variables.push({
355
+ name,
356
+ value: match[2].trim()
357
+ });
358
+ }
359
+ }
360
+
361
+ } catch (error) {
362
+ console.warn('[WuStyleBridge] ⚠️ Error detecting CSS variables:', error);
363
+ }
364
+
365
+ return variables;
366
+ }
367
+
368
+ /**
369
+ * 🎨 GENERATE CSS VARIABLES RESET: Genera el CSS que resetea las variables
370
+ * @param {Array<{name: string, value: string}>} variables - Variables a resetear
371
+ * @returns {string} CSS con el reset
372
+ */
373
+ generateCSSVariablesReset(variables) {
374
+ // 🔒 TÉCNICA: Redefinir cada variable como "unset" dentro del :host
375
+ // Esto rompe la cadena de herencia de CSS custom properties
376
+ //
377
+ // Problema: CSS variables se heredan a través de Shadow DOM por spec
378
+ // Solución: Redefinirlas explícitamente para "sobrescribir" el valor heredado
379
+ //
380
+ // Usamos valores por defecto del navegador para colores comunes
381
+ // y cadena vacía para otros
382
+
383
+ const resetRules = variables.map(v => {
384
+ const name = v.name;
385
+ // Detectar tipo de variable por su nombre y asignar valor neutro
386
+ if (name.includes('color') || name.includes('bg')) {
387
+ return ` ${name}: transparent;`;
388
+ } else if (name.includes('size') || name.includes('width') || name.includes('height')) {
389
+ return ` ${name}: auto;`;
390
+ } else if (name.includes('font')) {
391
+ return ` ${name}: inherit;`;
392
+ } else {
393
+ // Para otras variables, usar valor vacío que las invalida
394
+ return ` ${name}: initial;`;
395
+ }
396
+ }).join('\n');
397
+
398
+ return `
399
+ /* 🔒 WU-FRAMEWORK: CSS Variables Reset (fully-isolated mode)
400
+ * Este estilo BLOQUEA las CSS variables heredadas del padre
401
+ * para lograr un aislamiento visual completo.
402
+ * Variables bloqueadas: ${variables.length}
403
+ *
404
+ * El MFE debe definir sus propias variables CSS si las necesita.
405
+ * Las variables del padre/shell NO afectarán este componente.
406
+ */
407
+
408
+ :host {
409
+ /* 🛡️ Bloquear herencia de propiedades CSS del padre */
410
+ all: initial;
411
+ display: block;
412
+
413
+ /* 🔒 Redefinir variables del padre como valores neutros */
414
+ ${resetRules}
415
+ }
416
+
417
+ /* Restaurar herencia normal DENTRO del shadow DOM */
418
+ :host * {
419
+ all: revert;
420
+ }
421
+ `.trim();
422
+ }
423
+
424
+ /**
425
+ * 🔗 SET APP FOLDER: Asocia una carpeta del sistema de archivos con un MFE
426
+ * @param {string} appName - Nombre de la app
427
+ * @param {string} folderPath - Ruta de la carpeta (extraída de data-vite-dev-id)
428
+ */
429
+ setAppFolder(appName, folderPath) {
430
+ this.appFolders = this.appFolders || new Map();
431
+ this.appFolders.set(appName, folderPath.toLowerCase());
432
+ console.log(`[WuStyleBridge] 📁 Folder for ${appName}: ${folderPath}`);
433
+ }
434
+
435
+ /**
436
+ * 🔍 DETECTAR SI UN ESTILO PERTENECE A UN MFE
437
+ * @param {string} styleSource - URL o viteId del estilo
438
+ * @param {string} appName - Nombre de la app
439
+ * @param {string} appUrl - URL base del MFE
440
+ * @returns {boolean}
441
+ */
442
+ isStyleFromApp(styleSource, appName, appUrl) {
443
+ if (!styleSource) return false;
444
+
445
+ const source = styleSource.toLowerCase();
446
+
447
+ // 🎯 PRIORITY 1: Verificar puerto del MFE en localhost URL
448
+ // Ej: "http://localhost:3001/src/style.css" pertenece a MFE en puerto 3001
449
+ if (appUrl) {
450
+ try {
451
+ const appPort = new URL(appUrl).port;
452
+ if (appPort) {
453
+ // Si el estilo viene de localhost con el puerto del MFE
454
+ if (source.includes(`localhost:${appPort}`) ||
455
+ source.includes(`127.0.0.1:${appPort}`)) {
456
+ console.log(`[WuStyleBridge] ✅ Port match: ${appPort} for ${appName}`);
457
+ return true;
458
+ }
459
+ // Si el estilo viene de localhost pero con OTRO puerto, NO es de este MFE
460
+ const portMatch = source.match(/localhost:(\d+)|127\.0\.0\.1:(\d+)/);
461
+ if (portMatch) {
462
+ const stylePort = portMatch[1] || portMatch[2];
463
+ if (stylePort && stylePort !== appPort) {
464
+ return false;
465
+ }
466
+ }
467
+ }
468
+ } catch (e) {
469
+ // Ignore URL parse errors
470
+ }
471
+ }
472
+
473
+ // 🎯 PRIORITY 2: Usar la carpeta registrada del MFE
474
+ // Cuando el MFE carga, detectamos su carpeta y la guardamos
475
+ const appFolder = this.appFolders?.get(appName);
476
+ if (appFolder && source.includes(appFolder)) {
477
+ console.log(`[WuStyleBridge] ✅ Folder match for ${appName}`);
478
+ return true;
479
+ }
480
+
481
+ // 🎯 PRIORITY 3: Para file paths de Vite (data-vite-dev-id)
482
+ // Verificar si el path contiene la carpeta registrada del MFE
483
+ // La carpeta ya fue registrada desde wu-core.js con el valor del manifest
484
+ // Solo comparar, NO auto-detectar ni sobrescribir
485
+ if (appFolder) {
486
+ // El path debe contener la carpeta del MFE (e.g., /container/ o /header/)
487
+ // Normalizar la búsqueda para Windows y Unix paths
488
+ const folderPattern = appFolder.replace(/\//g, '[/\\\\]');
489
+ const folderRegex = new RegExp(folderPattern, 'i');
490
+ if (folderRegex.test(source)) {
491
+ console.log(`[WuStyleBridge] ✅ Folder regex match for ${appName}: ${appFolder}`);
492
+ return true;
493
+ }
494
+ }
495
+
496
+ return false;
497
+ }
498
+
185
499
  /**
186
500
  * 🔗 INYECTAR LINK STYLE: Clona <link> tag al Shadow DOM
187
501
  * @param {ShadowRoot} shadowRoot