wu-framework 1.0.6 → 1.1.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.
@@ -0,0 +1,726 @@
1
+ /**
2
+ * 🚀 WU-FRAMEWORK LIT ADAPTER
3
+ *
4
+ * Simplifica la integración de Lit (Web Components) con Wu Framework.
5
+ * Aprovecha los Web Components nativos con Shadow DOM incluido.
6
+ *
7
+ * @example
8
+ * // Microfrontend (main.js)
9
+ * import { wuLit } from 'wu-framework/adapters/lit';
10
+ * import { MyApp } from './my-app';
11
+ *
12
+ * wuLit.register('my-app', MyApp);
13
+ *
14
+ * @example
15
+ * // Usando LitElement
16
+ * import { LitElement, html, css } from 'lit';
17
+ *
18
+ * class MyApp extends LitElement {
19
+ * static styles = css`:host { display: block; }`;
20
+ *
21
+ * render() {
22
+ * return html`<h1>Hello from Lit!</h1>`;
23
+ * }
24
+ * }
25
+ *
26
+ * wuLit.register('my-app', MyApp);
27
+ */
28
+
29
+ // Estado global del adapter
30
+ const adapterState = {
31
+ apps: new Map(),
32
+ elements: new Map(),
33
+ lit: null,
34
+ initialized: false
35
+ };
36
+
37
+ /**
38
+ * Obtiene la instancia de Wu Framework
39
+ */
40
+ function getWuInstance() {
41
+ if (typeof window === 'undefined') return null;
42
+
43
+ return window.wu
44
+ || window.parent?.wu
45
+ || window.top?.wu
46
+ || null;
47
+ }
48
+
49
+ /**
50
+ * Espera a que Wu Framework esté disponible
51
+ */
52
+ function waitForWu(timeout = 5000) {
53
+ return new Promise((resolve, reject) => {
54
+ const wu = getWuInstance();
55
+ if (wu) {
56
+ resolve(wu);
57
+ return;
58
+ }
59
+
60
+ const startTime = Date.now();
61
+
62
+ const handleWuReady = () => {
63
+ cleanup();
64
+ resolve(getWuInstance());
65
+ };
66
+
67
+ window.addEventListener('wu:ready', handleWuReady);
68
+ window.addEventListener('wu:app:ready', handleWuReady);
69
+
70
+ const checkInterval = setInterval(() => {
71
+ const wu = getWuInstance();
72
+ if (wu) {
73
+ cleanup();
74
+ resolve(wu);
75
+ return;
76
+ }
77
+
78
+ if (Date.now() - startTime > timeout) {
79
+ cleanup();
80
+ reject(new Error(`Wu Framework not found after ${timeout}ms`));
81
+ }
82
+ }, 200);
83
+
84
+ function cleanup() {
85
+ clearInterval(checkInterval);
86
+ window.removeEventListener('wu:ready', handleWuReady);
87
+ window.removeEventListener('wu:app:ready', handleWuReady);
88
+ }
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Genera un nombre de tag válido para Custom Elements
94
+ */
95
+ function generateTagName(appName) {
96
+ // Custom elements deben tener un guión
97
+ if (appName.includes('-')) {
98
+ return `wu-${appName}`;
99
+ }
100
+ return `wu-app-${appName}`;
101
+ }
102
+
103
+ /**
104
+ * Registra un LitElement como microfrontend
105
+ *
106
+ * @param {string} appName - Nombre único del microfrontend
107
+ * @param {typeof LitElement} ElementClass - Clase que extiende LitElement
108
+ * @param {Object} options - Opciones adicionales
109
+ * @param {string} options.tagName - Nombre del custom element (auto-generado si no se provee)
110
+ * @param {Object} options.properties - Propiedades iniciales
111
+ * @param {Function} options.onMount - Callback después de montar
112
+ * @param {Function} options.onUnmount - Callback antes de desmontar
113
+ * @param {boolean} options.standalone - Permitir ejecución standalone (default: true)
114
+ * @param {string} options.standaloneContainer - Selector para modo standalone (default: '#root')
115
+ *
116
+ * @example
117
+ * import { LitElement, html } from 'lit';
118
+ *
119
+ * class HeaderApp extends LitElement {
120
+ * render() {
121
+ * return html`<header><h1>My Header</h1></header>`;
122
+ * }
123
+ * }
124
+ *
125
+ * wuLit.register('header', HeaderApp);
126
+ */
127
+ async function register(appName, ElementClass, options = {}) {
128
+ const {
129
+ tagName = null,
130
+ properties = {},
131
+ onMount = null,
132
+ onUnmount = null,
133
+ standalone = true,
134
+ standaloneContainer = '#root'
135
+ } = options;
136
+
137
+ // Generar nombre de tag
138
+ const customTagName = tagName || generateTagName(appName);
139
+
140
+ // Registrar el Custom Element si no existe
141
+ if (!customElements.get(customTagName)) {
142
+ try {
143
+ customElements.define(customTagName, ElementClass);
144
+ console.log(`[WuLit] Custom element <${customTagName}> defined`);
145
+ } catch (error) {
146
+ console.error(`[WuLit] Failed to define custom element:`, error);
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ // Guardar referencia de la clase
152
+ adapterState.elements.set(appName, {
153
+ ElementClass,
154
+ tagName: customTagName
155
+ });
156
+
157
+ // Función de mount
158
+ const mountApp = (container) => {
159
+ if (!container) {
160
+ console.error(`[WuLit] Mount failed for ${appName}: container is null`);
161
+ return;
162
+ }
163
+
164
+ // Evitar doble mount
165
+ if (adapterState.apps.has(appName)) {
166
+ console.warn(`[WuLit] ${appName} already mounted, unmounting first`);
167
+ unmountApp();
168
+ }
169
+
170
+ try {
171
+ // Limpiar container
172
+ container.innerHTML = '';
173
+
174
+ // Crear elemento
175
+ const element = document.createElement(customTagName);
176
+
177
+ // Aplicar propiedades
178
+ Object.entries(properties).forEach(([key, value]) => {
179
+ element[key] = value;
180
+ });
181
+
182
+ // Inyectar información de Wu
183
+ element.wuAppName = appName;
184
+ element.wuInstance = getWuInstance();
185
+
186
+ // Agregar al container
187
+ container.appendChild(element);
188
+
189
+ // Guardar referencia
190
+ adapterState.apps.set(appName, {
191
+ element,
192
+ container,
193
+ tagName: customTagName
194
+ });
195
+
196
+ console.log(`[WuLit] ✅ ${appName} (<${customTagName}>) mounted successfully`);
197
+
198
+ if (onMount) {
199
+ onMount(container, element);
200
+ }
201
+ } catch (error) {
202
+ console.error(`[WuLit] Mount error for ${appName}:`, error);
203
+ throw error;
204
+ }
205
+ };
206
+
207
+ // Función de unmount
208
+ const unmountApp = (container) => {
209
+ const appData = adapterState.apps.get(appName);
210
+
211
+ if (appData) {
212
+ try {
213
+ if (onUnmount) {
214
+ onUnmount(appData.container, appData.element);
215
+ }
216
+
217
+ // Remover elemento
218
+ if (appData.element && appData.element.parentNode) {
219
+ appData.element.remove();
220
+ }
221
+
222
+ // Limpiar container
223
+ appData.container.innerHTML = '';
224
+
225
+ adapterState.apps.delete(appName);
226
+
227
+ console.log(`[WuLit] ✅ ${appName} unmounted successfully`);
228
+ } catch (error) {
229
+ console.error(`[WuLit] Unmount error for ${appName}:`, error);
230
+ }
231
+ }
232
+
233
+ if (container) {
234
+ container.innerHTML = '';
235
+ }
236
+ };
237
+
238
+ // Intentar registrar con Wu Framework
239
+ try {
240
+ const wu = await waitForWu(3000);
241
+
242
+ wu.define(appName, {
243
+ mount: mountApp,
244
+ unmount: unmountApp
245
+ });
246
+
247
+ console.log(`[WuLit] ✅ ${appName} registered with Wu Framework`);
248
+ return true;
249
+
250
+ } catch (error) {
251
+ console.warn(`[WuLit] Wu Framework not available for ${appName}`);
252
+
253
+ if (standalone) {
254
+ const containerElement = document.querySelector(standaloneContainer);
255
+
256
+ if (containerElement) {
257
+ console.log(`[WuLit] Running ${appName} in standalone mode`);
258
+ mountApp(containerElement);
259
+ return true;
260
+ }
261
+ }
262
+
263
+ return false;
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Registra un Web Component vanilla (sin Lit) como microfrontend
269
+ *
270
+ * @param {string} appName - Nombre del microfrontend
271
+ * @param {typeof HTMLElement} ElementClass - Clase que extiende HTMLElement
272
+ * @param {Object} options - Opciones
273
+ *
274
+ * @example
275
+ * class MyWebComponent extends HTMLElement {
276
+ * connectedCallback() {
277
+ * this.attachShadow({ mode: 'open' });
278
+ * this.shadowRoot.innerHTML = '<h1>Hello!</h1>';
279
+ * }
280
+ * }
281
+ *
282
+ * wuLit.registerWebComponent('my-component', MyWebComponent);
283
+ */
284
+ async function registerWebComponent(appName, ElementClass, options = {}) {
285
+ // Usar el mismo registro pero para HTMLElement vanilla
286
+ return register(appName, ElementClass, options);
287
+ }
288
+
289
+ /**
290
+ * Crea un LitElement wrapper que carga un microfrontend
291
+ *
292
+ * @example
293
+ * import { html, LitElement } from 'lit';
294
+ * import { createWuSlotElement } from 'wu-framework/adapters/lit';
295
+ *
296
+ * const WuSlot = createWuSlotElement(LitElement, html);
297
+ *
298
+ * // Uso en otro componente
299
+ * render() {
300
+ * return html`<wu-slot name="header" url="http://localhost:3001"></wu-slot>`;
301
+ * }
302
+ */
303
+ function createWuSlotElement(LitElement, html, css) {
304
+ class WuSlotElement extends LitElement {
305
+ static properties = {
306
+ name: { type: String },
307
+ url: { type: String },
308
+ appName: { type: String, attribute: 'app-name' },
309
+ fallbackText: { type: String, attribute: 'fallback-text' },
310
+ loading: { type: Boolean, state: true },
311
+ error: { type: String, state: true }
312
+ };
313
+
314
+ static styles = css ? css`
315
+ :host {
316
+ display: block;
317
+ min-height: 100px;
318
+ position: relative;
319
+ }
320
+
321
+ .wu-slot-loading {
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ padding: 2rem;
326
+ color: #666;
327
+ }
328
+
329
+ .wu-slot-error {
330
+ padding: 1rem;
331
+ border: 1px solid #f5c6cb;
332
+ border-radius: 4px;
333
+ background: #f8d7da;
334
+ color: #721c24;
335
+ }
336
+
337
+ .wu-slot-error strong {
338
+ display: block;
339
+ margin-bottom: 0.5rem;
340
+ }
341
+
342
+ .wu-slot-content {
343
+ width: 100%;
344
+ height: 100%;
345
+ }
346
+ ` : [];
347
+
348
+ constructor() {
349
+ super();
350
+ this.name = '';
351
+ this.url = '';
352
+ this.appName = null;
353
+ this.fallbackText = null;
354
+ this.loading = true;
355
+ this.error = null;
356
+ this._appInstance = null;
357
+ }
358
+
359
+ get actualAppName() {
360
+ return this.appName || this.name;
361
+ }
362
+
363
+ async connectedCallback() {
364
+ super.connectedCallback();
365
+ await this.mountMicrofrontend();
366
+ }
367
+
368
+ disconnectedCallback() {
369
+ super.disconnectedCallback();
370
+ this.unmountMicrofrontend();
371
+ }
372
+
373
+ async mountMicrofrontend() {
374
+ try {
375
+ this.loading = true;
376
+ this.error = null;
377
+
378
+ const wu = getWuInstance();
379
+ if (!wu) {
380
+ throw new Error('Wu Framework not initialized');
381
+ }
382
+
383
+ // Esperar a que el componente se renderice
384
+ await this.updateComplete;
385
+
386
+ const contentSlot = this.shadowRoot.querySelector('.wu-slot-content');
387
+ if (!contentSlot) return;
388
+
389
+ const containerId = `wu-slot-${this.actualAppName}-${Date.now()}`;
390
+ const innerContainer = document.createElement('div');
391
+ innerContainer.id = containerId;
392
+ innerContainer.style.cssText = 'width: 100%; height: 100%;';
393
+
394
+ contentSlot.innerHTML = '';
395
+ contentSlot.appendChild(innerContainer);
396
+
397
+ const app = wu.app(this.actualAppName, {
398
+ url: this.url,
399
+ container: `#${containerId}`,
400
+ autoInit: true
401
+ });
402
+
403
+ this._appInstance = app;
404
+ await app.mount();
405
+
406
+ this.loading = false;
407
+ this.dispatchEvent(new CustomEvent('wu-load', {
408
+ detail: { name: this.actualAppName, url: this.url },
409
+ bubbles: true,
410
+ composed: true
411
+ }));
412
+
413
+ } catch (err) {
414
+ console.error(`[WuSlot] Error loading ${this.actualAppName}:`, err);
415
+ this.error = err.message || 'Failed to load microfrontend';
416
+ this.loading = false;
417
+ this.dispatchEvent(new CustomEvent('wu-error', {
418
+ detail: err,
419
+ bubbles: true,
420
+ composed: true
421
+ }));
422
+ }
423
+ }
424
+
425
+ async unmountMicrofrontend() {
426
+ if (this._appInstance) {
427
+ this.dispatchEvent(new CustomEvent('wu-unmount', {
428
+ detail: { name: this.actualAppName },
429
+ bubbles: true,
430
+ composed: true
431
+ }));
432
+
433
+ try {
434
+ await this._appInstance.unmount();
435
+ } catch (e) {}
436
+
437
+ this._appInstance = null;
438
+ }
439
+ }
440
+
441
+ render() {
442
+ if (this.error) {
443
+ return html`
444
+ <div class="wu-slot-error">
445
+ <strong>Error loading ${this.name}</strong>
446
+ <span>${this.error}</span>
447
+ </div>
448
+ `;
449
+ }
450
+
451
+ if (this.loading) {
452
+ return html`
453
+ <div class="wu-slot-loading">
454
+ ${this.fallbackText || `Loading ${this.name}...`}
455
+ </div>
456
+ <div class="wu-slot-content"></div>
457
+ `;
458
+ }
459
+
460
+ return html`<div class="wu-slot-content"></div>`;
461
+ }
462
+ }
463
+
464
+ // Registrar el elemento
465
+ if (!customElements.get('wu-slot')) {
466
+ customElements.define('wu-slot', WuSlotElement);
467
+ }
468
+
469
+ return WuSlotElement;
470
+ }
471
+
472
+ /**
473
+ * Mixin para agregar capacidades de Wu a cualquier LitElement
474
+ *
475
+ * @example
476
+ * import { LitElement } from 'lit';
477
+ * import { WuMixin } from 'wu-framework/adapters/lit';
478
+ *
479
+ * class MyElement extends WuMixin(LitElement) {
480
+ * connectedCallback() {
481
+ * super.connectedCallback();
482
+ *
483
+ * // Usar eventos de Wu
484
+ * this.wuOn('user:login', (data) => {
485
+ * console.log('User logged in:', data);
486
+ * });
487
+ * }
488
+ *
489
+ * handleClick() {
490
+ * this.wuEmit('button:clicked', { id: this.id });
491
+ * }
492
+ * }
493
+ */
494
+ function WuMixin(Base) {
495
+ return class extends Base {
496
+ constructor() {
497
+ super();
498
+ this._wuSubscriptions = [];
499
+ }
500
+
501
+ get wu() {
502
+ return getWuInstance();
503
+ }
504
+
505
+ // Event Bus methods
506
+ wuEmit(event, data, options) {
507
+ const wu = this.wu;
508
+ if (wu?.eventBus) {
509
+ wu.eventBus.emit(event, data, options);
510
+ }
511
+ }
512
+
513
+ wuOn(event, callback) {
514
+ const wu = this.wu;
515
+ if (wu?.eventBus) {
516
+ const unsubscribe = wu.eventBus.on(event, callback);
517
+ this._wuSubscriptions.push(unsubscribe);
518
+ return unsubscribe;
519
+ }
520
+ return () => {};
521
+ }
522
+
523
+ wuOnce(event, callback) {
524
+ const wu = this.wu;
525
+ if (wu?.eventBus) {
526
+ return wu.eventBus.once(event, callback);
527
+ }
528
+ return () => {};
529
+ }
530
+
531
+ // Store methods
532
+ wuGetState(path) {
533
+ const wu = this.wu;
534
+ return wu?.store?.get(path) || null;
535
+ }
536
+
537
+ wuSetState(path, value) {
538
+ const wu = this.wu;
539
+ if (wu?.store) {
540
+ wu.store.set(path, value);
541
+ }
542
+ }
543
+
544
+ wuOnStateChange(pattern, callback) {
545
+ const wu = this.wu;
546
+ if (wu?.store) {
547
+ const unsubscribe = wu.store.on(pattern, callback);
548
+ this._wuSubscriptions.push(unsubscribe);
549
+ return unsubscribe;
550
+ }
551
+ return () => {};
552
+ }
553
+
554
+ // Cleanup
555
+ disconnectedCallback() {
556
+ super.disconnectedCallback();
557
+ this._wuSubscriptions.forEach(unsub => unsub());
558
+ this._wuSubscriptions = [];
559
+ }
560
+ };
561
+ }
562
+
563
+ /**
564
+ * Decorador reactivo para propiedades conectadas al store de Wu
565
+ *
566
+ * @example
567
+ * import { LitElement } from 'lit';
568
+ * import { wuProperty } from 'wu-framework/adapters/lit';
569
+ *
570
+ * class MyElement extends LitElement {
571
+ * @wuProperty('user.name')
572
+ * userName;
573
+ *
574
+ * render() {
575
+ * return html`<p>Hello, ${this.userName}</p>`;
576
+ * }
577
+ * }
578
+ */
579
+ function wuProperty(storePath) {
580
+ return function(target, propertyKey) {
581
+ const privateKey = `_wu_${propertyKey}`;
582
+ let unsubscribe = null;
583
+
584
+ Object.defineProperty(target, propertyKey, {
585
+ get() {
586
+ return this[privateKey];
587
+ },
588
+ set(value) {
589
+ const wu = getWuInstance();
590
+ if (wu?.store) {
591
+ wu.store.set(storePath, value);
592
+ }
593
+ },
594
+ configurable: true,
595
+ enumerable: true
596
+ });
597
+
598
+ // Hook into connectedCallback
599
+ const originalConnected = target.connectedCallback;
600
+ target.connectedCallback = function() {
601
+ if (originalConnected) originalConnected.call(this);
602
+
603
+ const wu = getWuInstance();
604
+ if (wu?.store) {
605
+ // Set initial value
606
+ this[privateKey] = wu.store.get(storePath);
607
+
608
+ // Subscribe to changes
609
+ unsubscribe = wu.store.on(storePath, (value) => {
610
+ this[privateKey] = value;
611
+ this.requestUpdate();
612
+ });
613
+ }
614
+ };
615
+
616
+ // Hook into disconnectedCallback
617
+ const originalDisconnected = target.disconnectedCallback;
618
+ target.disconnectedCallback = function() {
619
+ if (originalDisconnected) originalDisconnected.call(this);
620
+ if (unsubscribe) {
621
+ unsubscribe();
622
+ unsubscribe = null;
623
+ }
624
+ };
625
+ };
626
+ }
627
+
628
+ /**
629
+ * Helper para crear un Web Component simple sin Lit
630
+ *
631
+ * @example
632
+ * const MyComponent = createSimpleElement({
633
+ * name: 'my-component',
634
+ * template: '<h1>Hello!</h1>',
635
+ * styles: ':host { display: block; color: blue; }',
636
+ * connectedCallback() {
637
+ * console.log('Connected!');
638
+ * }
639
+ * });
640
+ */
641
+ function createSimpleElement(config) {
642
+ const {
643
+ name,
644
+ template,
645
+ styles = '',
646
+ shadow = true,
647
+ ...callbacks
648
+ } = config;
649
+
650
+ class SimpleElement extends HTMLElement {
651
+ constructor() {
652
+ super();
653
+ if (shadow) {
654
+ this.attachShadow({ mode: 'open' });
655
+ }
656
+ }
657
+
658
+ connectedCallback() {
659
+ const root = this.shadowRoot || this;
660
+
661
+ if (styles) {
662
+ const styleEl = document.createElement('style');
663
+ styleEl.textContent = styles;
664
+ root.appendChild(styleEl);
665
+ }
666
+
667
+ if (typeof template === 'function') {
668
+ root.innerHTML += template(this);
669
+ } else {
670
+ root.innerHTML += template;
671
+ }
672
+
673
+ if (callbacks.connectedCallback) {
674
+ callbacks.connectedCallback.call(this);
675
+ }
676
+ }
677
+
678
+ disconnectedCallback() {
679
+ if (callbacks.disconnectedCallback) {
680
+ callbacks.disconnectedCallback.call(this);
681
+ }
682
+ }
683
+
684
+ attributeChangedCallback(name, oldVal, newVal) {
685
+ if (callbacks.attributeChangedCallback) {
686
+ callbacks.attributeChangedCallback.call(this, name, oldVal, newVal);
687
+ }
688
+ }
689
+ }
690
+
691
+ if (callbacks.observedAttributes) {
692
+ SimpleElement.observedAttributes = callbacks.observedAttributes;
693
+ }
694
+
695
+ if (!customElements.get(name)) {
696
+ customElements.define(name, SimpleElement);
697
+ }
698
+
699
+ return SimpleElement;
700
+ }
701
+
702
+ // API pública del adapter
703
+ export const wuLit = {
704
+ register,
705
+ registerWebComponent,
706
+ createWuSlotElement,
707
+ WuMixin,
708
+ wuProperty,
709
+ createSimpleElement,
710
+ getWuInstance,
711
+ waitForWu
712
+ };
713
+
714
+ // Named exports
715
+ export {
716
+ register,
717
+ registerWebComponent,
718
+ createWuSlotElement,
719
+ WuMixin,
720
+ wuProperty,
721
+ createSimpleElement,
722
+ getWuInstance,
723
+ waitForWu
724
+ };
725
+
726
+ export default wuLit;