@vnb/mfe-events 0.0.1-rc.1

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