@vnb/mfe-events 1.0.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/dist/index.js ADDED
@@ -0,0 +1,784 @@
1
+ /**
2
+ * @vnb/mfe-events v1.0.0
3
+ * Micro frontend event plugin for cross-app communication with type safety
4
+ * License: MIT
5
+ */
6
+ 'use strict';
7
+
8
+ Object.defineProperty(exports, '__esModule', { value: true });
9
+
10
+ /**
11
+ * @vnb/mfe-events — Transport Layer
12
+ *
13
+ * Provides abstraction for event transmission across different environments:
14
+ * - MemoryTransport: Shell internal (no window)
15
+ * - WindowTransport: MF/NF Remote Apps (CustomEvent)
16
+ * - PostMessageTransport: iframe Remote Apps (postMessage)
17
+ * - IframeProxyTransport: Shell → iframe proxy (postMessage)
18
+ */
19
+ /**
20
+ * MemoryTransport — Shell internal event bus
21
+ * Uses in-memory EventTarget, does not touch window
22
+ */
23
+ class MemoryTransport {
24
+ constructor() {
25
+ this.target = new EventTarget();
26
+ }
27
+ emit(event, data) {
28
+ this.target.dispatchEvent(new CustomEvent(event, { detail: data }));
29
+ }
30
+ on(event, handler) {
31
+ const wrapper = (e) => handler(e.detail);
32
+ this.target.addEventListener(event, wrapper);
33
+ return () => this.target.removeEventListener(event, wrapper);
34
+ }
35
+ }
36
+ /**
37
+ * WindowTransport — MF/NF Remote Apps communication
38
+ * Uses window.CustomEvent for same-tab communication
39
+ */
40
+ class WindowTransport {
41
+ emit(event, data) {
42
+ window.dispatchEvent(new CustomEvent(event, { detail: data }));
43
+ }
44
+ on(event, handler) {
45
+ const wrapper = (e) => handler(e.detail);
46
+ window.addEventListener(event, wrapper);
47
+ return () => window.removeEventListener(event, wrapper);
48
+ }
49
+ }
50
+ /**
51
+ * PostMessageTransport — iframe Remote Apps communication
52
+ * Uses window.postMessage for cross-origin communication
53
+ */
54
+ class PostMessageTransport {
55
+ constructor(options = {}) {
56
+ this.targetWindow = options.targetWindow || window.parent;
57
+ this.targetOrigin = options.targetOrigin || '*';
58
+ }
59
+ emit(event, data) {
60
+ this.targetWindow.postMessage({ scope: 'mfe', event, data, ts: Date.now() }, this.targetOrigin);
61
+ }
62
+ on(event, handler) {
63
+ const wrapper = (e) => {
64
+ if (e.data?.scope === 'mfe' && e.data?.event === event) {
65
+ handler(e.data.data);
66
+ }
67
+ };
68
+ window.addEventListener('message', wrapper);
69
+ return () => window.removeEventListener('message', wrapper);
70
+ }
71
+ }
72
+ /**
73
+ * IframeProxyTransport — Shell side proxy for iframe communication
74
+ * Sends postMessage to specific iframe and receives messages from it
75
+ */
76
+ class IframeProxyTransport {
77
+ constructor(iframe, targetOrigin = '*') {
78
+ this.iframe = iframe;
79
+ this.targetOrigin = targetOrigin;
80
+ }
81
+ emit(event, data) {
82
+ this.iframe.contentWindow?.postMessage({ scope: 'mfe', event, data, ts: Date.now() }, this.targetOrigin);
83
+ }
84
+ on(event, handler) {
85
+ const wrapper = (e) => {
86
+ // Only receive messages from this specific iframe
87
+ if (e.source === this.iframe.contentWindow &&
88
+ e.data?.scope === 'mfe' &&
89
+ e.data?.event === event) {
90
+ handler(e.data.data);
91
+ }
92
+ };
93
+ window.addEventListener('message', wrapper);
94
+ return () => window.removeEventListener('message', wrapper);
95
+ }
96
+ }
97
+
98
+ var transport = /*#__PURE__*/Object.freeze({
99
+ __proto__: null,
100
+ IframeProxyTransport: IframeProxyTransport,
101
+ MemoryTransport: MemoryTransport,
102
+ PostMessageTransport: PostMessageTransport,
103
+ WindowTransport: WindowTransport
104
+ });
105
+
106
+ /**
107
+ * @vnb/mfe-events — Type-safe Event Bus
108
+ *
109
+ * Provides compile-time type checking for event names and payloads.
110
+ * Wraps Transport with generic type constraints.
111
+ */
112
+ class Bus {
113
+ constructor(transport) {
114
+ this.transport = transport;
115
+ }
116
+ /**
117
+ * Emit an event with type-safe payload
118
+ */
119
+ emit(event, data) {
120
+ this.transport.emit(event, data);
121
+ }
122
+ /**
123
+ * Subscribe to an event with type-safe handler
124
+ * Returns unsubscribe function
125
+ */
126
+ on(event, handler, options) {
127
+ if (options?.once) {
128
+ return this.once(event, handler);
129
+ }
130
+ return this.transport.on(event, handler);
131
+ }
132
+ /**
133
+ * Subscribe to an event once
134
+ * Returns unsubscribe function
135
+ */
136
+ once(event, handler) {
137
+ const off = this.on(event, (data) => {
138
+ off();
139
+ handler(data);
140
+ });
141
+ return off;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * @vnb/mfe-events — Event Constants
147
+ *
148
+ * Three scopes:
149
+ * - EvtShl: Shell internal (no window, memory only)
150
+ * - EvtMfe: Cross-app (Shell + Remote Apps)
151
+ * - EvtApp: Remote App internal (framework-specific)
152
+ *
153
+ * Naming convention: {scope}:{domain}-{action}
154
+ * - State/notification events: must have -{action} suffix
155
+ * - Event markers (error, shell-ready, raw): no suffix
156
+ */
157
+ // ─────────────────────────────────────────────
158
+ // Shell Internal — no window, memory only
159
+ // ─────────────────────────────────────────────
160
+ const EvtShl = {
161
+ TabOverflow: 'shl:tab-overflow',
162
+ TabClose: 'shl:tab-close',
163
+ RouteReuse: 'shl:route-reuse',
164
+ AuthSync: 'shl:auth-sync',
165
+ StateSync: 'shl:state-sync',
166
+ AppMounted: 'shl:app-mounted',
167
+ AppUnmounted: 'shl:app-unmounted',
168
+ };
169
+ // ─────────────────────────────────────────────
170
+ // Cross-App — Shell and Remote Apps shared protocol
171
+ // ─────────────────────────────────────────────
172
+ const EvtMfe = {
173
+ // Global state domain
174
+ ThemeUpdate: 'mfe:theme-update',
175
+ LocaleUpdate: 'mfe:locale-update',
176
+ BreakpointUpdate: 'mfe:breakpoint-update',
177
+ StateUpdate: 'mfe:state-update',
178
+ ShellReady: 'mfe:shell-ready',
179
+ Error: 'mfe:error',
180
+ Raw: 'mfe:raw',
181
+ // Auth domain
182
+ AuthLogin: 'mfe:auth-login',
183
+ AuthLogout: 'mfe:auth-logout',
184
+ AuthTokenUpdate: 'mfe:auth-token-update',
185
+ AuthTokenClear: 'mfe:auth-token-clear',
186
+ AuthPermissionUpdate: 'mfe:auth-permission-update',
187
+ AuthExpired: 'mfe:auth-expired',
188
+ // Route domain
189
+ RouteNavigate: 'mfe:route-navigate',
190
+ RouteRefresh: 'mfe:route-refresh',
191
+ RouteChange: 'mfe:route-change',
192
+ // Lifecycle domain
193
+ LifecycleAppClose: 'mfe:lifecycle-app-close',
194
+ LifecycleAppReady: 'mfe:lifecycle-app-ready',
195
+ LifecycleAppUnmount: 'mfe:lifecycle-app-unmount',
196
+ // Notification domain
197
+ NotifyShow: 'mfe:notify-show',
198
+ NotifyClose: 'mfe:notify-close',
199
+ NotifyCloseAll: 'mfe:notify-close-all',
200
+ };
201
+ // ─────────────────────────────────────────────
202
+ // Remote App Internal — defined by each app
203
+ // ─────────────────────────────────────────────
204
+ function EvtApp(appId, name) {
205
+ return `app:${appId}-${name}`;
206
+ }
207
+
208
+ /**
209
+ * @vnb/mfe-events — Plugin Core
210
+ *
211
+ * MfeEventPlugin manages Remote App registration and provides:
212
+ * - register/unregister for Remote Apps
213
+ * - emitTo for targeted communication
214
+ * - emit for broadcast communication
215
+ * - Notification API (success/info/warning/danger)
216
+ * - Automatic iframe message proxying
217
+ */
218
+ class MfeEventPlugin {
219
+ constructor() {
220
+ this.registry = new Map();
221
+ this._notifyId = 0;
222
+ this._shellReady = false;
223
+ this._notifyContainer = null;
224
+ this._notifyItems = new Map();
225
+ this._pendingClosers = new Map();
226
+ this._customRenderer = null;
227
+ this._customCloser = null;
228
+ this._customClear = null;
229
+ if (typeof window === 'undefined')
230
+ return;
231
+ // Shell side: globally listen for iframe messages and forward as CustomEvent
232
+ if (window.self === window.top) {
233
+ window.addEventListener('message', (e) => {
234
+ if (e.data?.scope === 'mfe' && e.data?.event) {
235
+ window.dispatchEvent(new CustomEvent(e.data.event, { detail: e.data.data }));
236
+ }
237
+ });
238
+ }
239
+ // Always init DOM fallback for notifications
240
+ this._initDOMFallback();
241
+ }
242
+ // ─────────────────────────────────────────
243
+ // Custom Renderer (for Shell integration)
244
+ // ─────────────────────────────────────────
245
+ /**
246
+ * Register custom notification renderer (Shell use)
247
+ * When registered, DOM fallback is disabled
248
+ */
249
+ useRenderer(render, close, clear) {
250
+ this._customRenderer = render;
251
+ this._customCloser = close || null;
252
+ this._customClear = clear || null;
253
+ }
254
+ // ─────────────────────────────────────────
255
+ // DOM Fallback Renderer
256
+ // ─────────────────────────────────────────
257
+ _initDOMFallback() {
258
+ // Inject styles
259
+ const styleId = 'mfe-notify-styles';
260
+ if (!document.getElementById(styleId)) {
261
+ const style = document.createElement('style');
262
+ style.id = styleId;
263
+ style.textContent = `
264
+ .mfe-notify-host {
265
+ position: fixed;
266
+ top: 16px;
267
+ left: 50%;
268
+ transform: translateX(-50%);
269
+ z-index: 99999;
270
+ display: flex;
271
+ flex-direction: column;
272
+ gap: 8px;
273
+ max-width: 480px;
274
+ width: calc(100% - 32px);
275
+ pointer-events: none;
276
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
277
+ }
278
+ .mfe-notify-item {
279
+ display: flex;
280
+ align-items: flex-start;
281
+ gap: 10px;
282
+ padding: 12px 14px;
283
+ background: #ffffff;
284
+ border: 1px solid rgba(0, 0, 0, 0.08);
285
+ border-left: 4px solid var(--mfe-notify-color, #6b7280);
286
+ border-radius: 8px;
287
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
288
+ pointer-events: auto;
289
+ animation: mfe-notify-slide-in 180ms ease-out;
290
+ }
291
+ .mfe-notify-item[data-type="success"] { --mfe-notify-color: #16a34a; }
292
+ .mfe-notify-item[data-type="info"] { --mfe-notify-color: #3b82f6; }
293
+ .mfe-notify-item[data-type="warning"] { --mfe-notify-color: #d97706; }
294
+ .mfe-notify-item[data-type="danger"] { --mfe-notify-color: #dc2626; }
295
+ .mfe-notify-icon {
296
+ flex: 0 0 24px;
297
+ width: 24px;
298
+ height: 24px;
299
+ border-radius: 50%;
300
+ display: flex;
301
+ align-items: center;
302
+ justify-content: center;
303
+ font-size: 14px;
304
+ font-weight: 700;
305
+ color: #fff;
306
+ background: var(--mfe-notify-color, #6b7280);
307
+ }
308
+ .mfe-notify-body { flex: 1; min-width: 0; }
309
+ .mfe-notify-title {
310
+ font-size: 13px;
311
+ font-weight: 600;
312
+ color: #1f2937;
313
+ margin-bottom: 2px;
314
+ }
315
+ .mfe-notify-message {
316
+ font-size: 13px;
317
+ line-height: 1.4;
318
+ color: #374151;
319
+ word-break: break-word;
320
+ }
321
+ .mfe-notify-close {
322
+ flex: 0 0 24px;
323
+ width: 24px;
324
+ height: 24px;
325
+ border: none;
326
+ background: none;
327
+ color: #6b7280;
328
+ cursor: pointer;
329
+ font-size: 18px;
330
+ line-height: 1;
331
+ border-radius: 4px;
332
+ }
333
+ .mfe-notify-close:hover { background: rgba(0, 0, 0, 0.06); color: #111827; }
334
+ .mfe-notify-actions {
335
+ display: flex;
336
+ gap: 8px;
337
+ margin-top: 8px;
338
+ }
339
+ .mfe-notify-btn {
340
+ padding: 4px 12px;
341
+ font-size: 12px;
342
+ font-weight: 500;
343
+ border: 1px solid rgba(0, 0, 0, 0.15);
344
+ border-radius: 4px;
345
+ background: #fff;
346
+ color: #374151;
347
+ cursor: pointer;
348
+ transition: background 0.15s, border-color 0.15s;
349
+ }
350
+ .mfe-notify-btn:hover {
351
+ background: #f3f4f6;
352
+ border-color: rgba(0, 0, 0, 0.25);
353
+ }
354
+ .mfe-notify-btn:active {
355
+ background: #e5e7eb;
356
+ }
357
+ @keyframes mfe-notify-slide-in {
358
+ from { transform: translateY(-12px); opacity: 0; }
359
+ to { transform: translateY(0); opacity: 1; }
360
+ }
361
+ `;
362
+ document.head.appendChild(style);
363
+ }
364
+ // Create container
365
+ this._notifyContainer = document.createElement('div');
366
+ this._notifyContainer.className = 'mfe-notify-host';
367
+ this._notifyContainer.setAttribute('aria-live', 'polite');
368
+ document.body.appendChild(this._notifyContainer);
369
+ }
370
+ _renderToast(config) {
371
+ if (!this._notifyContainer)
372
+ return;
373
+ const { id, type = 'info', message = '', title, duration = 3000, buttons = [] } = config;
374
+ const icons = {
375
+ success: '✓',
376
+ info: 'i',
377
+ warning: '!',
378
+ danger: '✕',
379
+ };
380
+ // Escape HTML to prevent XSS
381
+ const escapeHtml = (str) => {
382
+ const div = document.createElement('div');
383
+ div.textContent = str;
384
+ return div.innerHTML;
385
+ };
386
+ const item = document.createElement('div');
387
+ item.className = 'mfe-notify-item';
388
+ item.setAttribute('data-type', type);
389
+ item.setAttribute('data-notify-id', id);
390
+ // Build buttons HTML
391
+ let buttonsHtml = '';
392
+ if (buttons.length > 0) {
393
+ buttonsHtml = `<div class="mfe-notify-actions">${buttons.map((btn, idx) => `<button class="mfe-notify-btn" data-btn-idx="${idx}">${escapeHtml(btn.text)}</button>`).join('')}</div>`;
394
+ }
395
+ item.innerHTML = `
396
+ <div class="mfe-notify-icon">${icons[type] || 'i'}</div>
397
+ <div class="mfe-notify-body">
398
+ ${title ? `<div class="mfe-notify-title">${escapeHtml(title)}</div>` : ''}
399
+ <div class="mfe-notify-message">${escapeHtml(message)}</div>
400
+ ${buttonsHtml}
401
+ </div>
402
+ <button class="mfe-notify-close" type="button" aria-label="Dismiss">×</button>
403
+ `;
404
+ // Bind button handlers
405
+ buttons.forEach((btn, idx) => {
406
+ const btnEl = item.querySelector(`[data-btn-idx="${idx}"]`);
407
+ if (btnEl && btn.handler) {
408
+ btnEl.addEventListener('click', () => {
409
+ btn.handler();
410
+ if (btn.role !== 'sticky') {
411
+ this._removeToast(id);
412
+ }
413
+ });
414
+ }
415
+ });
416
+ const closeBtn = item.querySelector('.mfe-notify-close');
417
+ closeBtn.addEventListener('click', () => this._removeToast(id));
418
+ this._notifyContainer.appendChild(item);
419
+ this._notifyItems.set(id, item);
420
+ if (duration > 0) {
421
+ setTimeout(() => this._removeToast(id), duration);
422
+ }
423
+ }
424
+ _removeToast(id) {
425
+ const item = this._notifyItems.get(id);
426
+ if (item) {
427
+ item.remove();
428
+ this._notifyItems.delete(id);
429
+ }
430
+ this._pendingClosers.delete(id);
431
+ }
432
+ _removeAllToasts() {
433
+ this._notifyItems.forEach((item) => item.remove());
434
+ this._notifyItems.clear();
435
+ this._pendingClosers.clear();
436
+ }
437
+ // ─────────────────────────────────────────
438
+ // Remote App Registration
439
+ // ─────────────────────────────────────────
440
+ /**
441
+ * Register a Remote App with event handlers
442
+ */
443
+ register(options) {
444
+ if (this.registry.has(options.appId)) {
445
+ this.unregister(options.appId);
446
+ }
447
+ const meta = {
448
+ appId: options.appId,
449
+ type: options.type || 'mf',
450
+ };
451
+ const cleanups = [];
452
+ // Auto-subscribe handlers
453
+ Object.entries(options.handlers).forEach(([event, handler]) => {
454
+ if (!handler)
455
+ return;
456
+ if (options.type === 'iframe') {
457
+ // iframe: listen via postMessage
458
+ const wrapper = (e) => {
459
+ if (e.data?.scope === 'mfe' && e.data?.event === event) {
460
+ handler(e.data.data);
461
+ }
462
+ };
463
+ window.addEventListener('message', wrapper);
464
+ cleanups.push(() => window.removeEventListener('message', wrapper));
465
+ }
466
+ else {
467
+ // MF/NF: listen via window.CustomEvent
468
+ const wrapper = (e) => handler(e.detail);
469
+ window.addEventListener(event, wrapper);
470
+ cleanups.push(() => window.removeEventListener(event, wrapper));
471
+ }
472
+ });
473
+ this.registry.set(options.appId, { meta, options, cleanups });
474
+ // fallback 存入 registry,不覆盖全局 _customRenderer
475
+ // 全局渲染器仅由 Shell 的 useRenderer() 设置
476
+ }
477
+ /**
478
+ * Register an iframe Remote App (Shell side)
479
+ */
480
+ registerIframe(appId, iframe, targetOrigin) {
481
+ // Lazy import to avoid bundling issues
482
+ Promise.resolve().then(function () { return transport; }).then(({ IframeProxyTransport }) => {
483
+ const transport = new IframeProxyTransport(iframe, targetOrigin);
484
+ this.registry.set(appId, {
485
+ meta: { appId, type: 'iframe', transport },
486
+ options: { appId, type: 'iframe', handlers: {} },
487
+ cleanups: [],
488
+ });
489
+ });
490
+ }
491
+ /**
492
+ * Unregister a Remote App
493
+ */
494
+ unregister(appId) {
495
+ const entry = this.registry.get(appId);
496
+ if (entry) {
497
+ entry.cleanups.forEach((fn) => fn());
498
+ }
499
+ this.registry.delete(appId);
500
+ }
501
+ // ─────────────────────────────────────────
502
+ // Shell Targeted Communication
503
+ // ─────────────────────────────────────────
504
+ /**
505
+ * Send event to a specific Remote App
506
+ */
507
+ emitTo(appId, event, data) {
508
+ const app = this.registry.get(appId);
509
+ if (!app)
510
+ return;
511
+ if (app.meta.type === 'iframe' && app.meta.transport) {
512
+ // iframe: send via postMessage
513
+ app.meta.transport.emit(event, data);
514
+ }
515
+ else {
516
+ // MF: call handler directly or dispatch CustomEvent
517
+ const handler = app.options.handlers[event];
518
+ if (handler) {
519
+ handler(data);
520
+ }
521
+ }
522
+ }
523
+ // ─────────────────────────────────────────
524
+ // Shell Broadcast Communication
525
+ // ─────────────────────────────────────────
526
+ /**
527
+ * Broadcast event to all Remote Apps (MF + iframe)
528
+ */
529
+ emit(event, data) {
530
+ // Broadcast to all MF Remote Apps
531
+ window.dispatchEvent(new CustomEvent(event, { detail: data }));
532
+ // Broadcast to all iframe Remote Apps
533
+ this.registry.forEach(({ meta }) => {
534
+ if (meta.type === 'iframe' && meta.transport) {
535
+ meta.transport.emit(event, data);
536
+ }
537
+ });
538
+ }
539
+ // ─────────────────────────────────────────
540
+ // Notification API
541
+ // ─────────────────────────────────────────
542
+ /**
543
+ * Build notification config from flexible arguments
544
+ * Supports: success("msg"), success({message:"msg",duration:5000}), success("msg","title",{duration:5000})
545
+ */
546
+ _buildConfig(type, message, args) {
547
+ const config = { type };
548
+ if (typeof message === 'string') {
549
+ config.message = message;
550
+ }
551
+ else if (message && typeof message === 'object') {
552
+ Object.assign(config, message);
553
+ }
554
+ args.forEach((arg) => {
555
+ if (typeof arg === 'string') {
556
+ config.title = arg;
557
+ }
558
+ else if (arg && typeof arg === 'object') {
559
+ Object.assign(config, arg);
560
+ }
561
+ });
562
+ return config;
563
+ }
564
+ /**
565
+ * Show a notification
566
+ */
567
+ notify(config) {
568
+ if (typeof window === 'undefined')
569
+ return { close: () => { } };
570
+ const id = `mfe-${++this._notifyId}`;
571
+ const finalConfig = {
572
+ duration: 3000,
573
+ ...config,
574
+ id,
575
+ };
576
+ // 1. Always dispatch CustomEvent (for Shell listeners)
577
+ window.dispatchEvent(new CustomEvent(EvtMfe.NotifyShow, {
578
+ detail: finalConfig,
579
+ bubbles: true,
580
+ cancelable: true,
581
+ }));
582
+ // 2. Render DOM fallback (or use custom renderer)
583
+ if (this._customRenderer) {
584
+ this._customRenderer(finalConfig);
585
+ }
586
+ else {
587
+ this._renderToast(finalConfig);
588
+ }
589
+ // 3. Track closer for closeAll()
590
+ const closeFn = () => {
591
+ // Dispatch close event
592
+ window.dispatchEvent(new CustomEvent(EvtMfe.NotifyClose, {
593
+ detail: { id },
594
+ bubbles: true,
595
+ }));
596
+ // Remove DOM toast
597
+ if (this._customCloser) {
598
+ this._customCloser(id);
599
+ }
600
+ else {
601
+ this._removeToast(id);
602
+ }
603
+ };
604
+ this._pendingClosers.set(id, closeFn);
605
+ return {
606
+ close: () => {
607
+ const closer = this._pendingClosers.get(id);
608
+ if (closer) {
609
+ closer();
610
+ this._pendingClosers.delete(id);
611
+ }
612
+ },
613
+ };
614
+ }
615
+ /**
616
+ * Show success notification
617
+ */
618
+ success(message, ...args) {
619
+ return this.notify(this._buildConfig('success', message, args));
620
+ }
621
+ /**
622
+ * Show info notification
623
+ */
624
+ info(message, ...args) {
625
+ return this.notify(this._buildConfig('info', message, args));
626
+ }
627
+ /**
628
+ * Show warning notification
629
+ */
630
+ warning(message, ...args) {
631
+ return this.notify(this._buildConfig('warning', message, args));
632
+ }
633
+ /**
634
+ * Show danger notification
635
+ */
636
+ danger(message, ...args) {
637
+ return this.notify(this._buildConfig('danger', message, args));
638
+ }
639
+ /**
640
+ * Close all notifications
641
+ */
642
+ closeAll() {
643
+ // Call all pending closers
644
+ this._pendingClosers.forEach((closer) => closer());
645
+ this._pendingClosers.clear();
646
+ // Dispatch close-all event
647
+ window.dispatchEvent(new CustomEvent(EvtMfe.NotifyCloseAll, {
648
+ detail: {},
649
+ bubbles: true,
650
+ }));
651
+ // Clear DOM toasts
652
+ if (this._customClear) {
653
+ this._customClear();
654
+ }
655
+ else {
656
+ this._removeAllToasts();
657
+ }
658
+ }
659
+ // ─────────────────────────────────────────
660
+ // Lifecycle Helpers
661
+ // ─────────────────────────────────────────
662
+ /**
663
+ * Mark Shell as ready
664
+ */
665
+ shellReady() {
666
+ setTimeout(() => {
667
+ window.dispatchEvent(new CustomEvent(EvtMfe.ShellReady, {
668
+ detail: {},
669
+ bubbles: true,
670
+ cancelable: true,
671
+ }));
672
+ this._shellReady = true;
673
+ }, 100);
674
+ }
675
+ /**
676
+ * Check if Shell is ready
677
+ */
678
+ isShellReady() {
679
+ return !!this._shellReady;
680
+ }
681
+ // ─────────────────────────────────────────
682
+ // Auth Helpers (convenience methods)
683
+ // ─────────────────────────────────────────
684
+ /**
685
+ * Broadcast auth login event
686
+ */
687
+ authLogin(user) {
688
+ this.emit(EvtMfe.AuthLogin, { user, token: '' });
689
+ }
690
+ /**
691
+ * Broadcast auth logout event
692
+ */
693
+ authLogout(reason) {
694
+ this.emit(EvtMfe.AuthLogout, { reason: reason || 'user' });
695
+ }
696
+ /**
697
+ * Broadcast token update event
698
+ */
699
+ tokenUpdate(data) {
700
+ this.emit(EvtMfe.AuthTokenUpdate, data);
701
+ }
702
+ /**
703
+ * Broadcast auth expired event
704
+ */
705
+ authExpired() {
706
+ this.emit(EvtMfe.AuthExpired, {});
707
+ }
708
+ /**
709
+ * Broadcast locale update event
710
+ */
711
+ locale(locale) {
712
+ this.emit(EvtMfe.LocaleUpdate, { locale });
713
+ }
714
+ }
715
+ // Singleton export
716
+ const mfeEventPlugin = new MfeEventPlugin();
717
+
718
+ /**
719
+ * @vnb/mfe-events — Micro Frontend Event Plugin
720
+ *
721
+ * Unified event communication for Module Federation / Native Federation / iframe architectures.
722
+ *
723
+ * Features:
724
+ * - Three scopes: EvtShl (Shell internal), EvtMfe (cross-app), EvtApp (Remote App internal)
725
+ * - Type-safe event bus with compile-time checking
726
+ * - Automatic Transport adaptation (CustomEvent for MF, postMessage for iframe)
727
+ * - Plugin API for Remote App registration and communication
728
+ * - Notification API with fallback support
729
+ *
730
+ * Usage:
731
+ * ```typescript
732
+ * import mfeEventPlugin, { mfeBus, shlBus } from '@vnb/mfe-events'
733
+ * import { EvtMfe, EvtShl, EvtApp } from '@vnb/mfe-events'
734
+ * ```
735
+ */
736
+ // Transport layer
737
+ /**
738
+ * Shell internal bus — isolated, does not touch window
739
+ * Use for Shell-internal state synchronization
740
+ */
741
+ const shlBus = new Bus(new MemoryTransport());
742
+ /**
743
+ * Cross-app bus — auto-detects environment and selects Transport
744
+ * - Shell/MF/NF: WindowTransport (CustomEvent)
745
+ * - iframe: PostMessageTransport (postMessage)
746
+ */
747
+ function createMfeBus() {
748
+ if (typeof window === 'undefined') {
749
+ // SSR fallback — use MemoryTransport
750
+ return new Bus(new MemoryTransport());
751
+ }
752
+ if (window.self !== window.top) {
753
+ // iframe environment → send to parent via postMessage
754
+ return new Bus(new PostMessageTransport());
755
+ }
756
+ // Shell/MF/NF environment → use window.CustomEvent
757
+ // Federation modules may not share the same instance,
758
+ // so window is used as the shared channel fallback
759
+ return new Bus(new WindowTransport());
760
+ }
761
+ const mfeBus = createMfeBus();
762
+ // ─────────────────────────────────────────────
763
+ // Global variable for backward compatibility
764
+ // ─────────────────────────────────────────────
765
+ if (typeof window !== 'undefined') {
766
+ const win = window;
767
+ if (!win.__MFE_EVENT_PLUGIN__) {
768
+ win.__MFE_EVENT_PLUGIN__ = mfeEventPlugin;
769
+ }
770
+ }
771
+
772
+ exports.Bus = Bus;
773
+ exports.EvtApp = EvtApp;
774
+ exports.EvtMfe = EvtMfe;
775
+ exports.EvtShl = EvtShl;
776
+ exports.IframeProxyTransport = IframeProxyTransport;
777
+ exports.MemoryTransport = MemoryTransport;
778
+ exports.PostMessageTransport = PostMessageTransport;
779
+ exports.WindowTransport = WindowTransport;
780
+ exports.default = mfeEventPlugin;
781
+ exports.mfeBus = mfeBus;
782
+ exports.mfeEventPlugin = mfeEventPlugin;
783
+ exports.shlBus = shlBus;
784
+ //# sourceMappingURL=index.js.map