@elliemae/pui-app-bridge 2.24.0 → 2.26.0-alpha.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/cjs/appBridge.js +173 -26
- package/dist/cjs/config/app.js +8 -0
- package/dist/cjs/frame.html +0 -21
- package/dist/cjs/loaders/script.js +12 -12
- package/dist/cjs/tests/serverHandlers.js +0 -8
- package/dist/esm/appBridge.js +173 -26
- package/dist/esm/config/app.js +8 -0
- package/dist/esm/frame.html +0 -21
- package/dist/esm/loaders/script.js +12 -12
- package/dist/esm/tests/serverHandlers.js +0 -8
- package/dist/public/assets/{frame.671d9de68be598da64ca.html → frame.4cbbcfa9ded96b660559.html} +0 -21
- package/dist/public/e2e-host.html +1 -1
- package/dist/public/e2e-index.html +1 -1
- package/dist/public/frame.html +1 -1
- package/dist/public/index.html +1 -1
- package/dist/public/js/emuiAppBridge.dc4dbeb6feea171656da.js +17 -0
- package/dist/public/js/emuiAppBridge.dc4dbeb6feea171656da.js.br +0 -0
- package/dist/public/js/emuiAppBridge.dc4dbeb6feea171656da.js.gz +0 -0
- package/dist/public/js/emuiAppBridge.dc4dbeb6feea171656da.js.map +1 -0
- package/dist/types/lib/appBridge.d.ts +11 -2
- package/dist/types/lib/config/app.d.ts +6 -0
- package/dist/types/lib/loaders/script.d.ts +5 -3
- package/dist/types/lib/typings/host.d.ts +5 -0
- package/dist/types/lib/typings/window.d.ts +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/umd/{671d9de68be598da64ca.html → 4cbbcfa9ded96b660559.html} +0 -21
- package/dist/umd/frame.html +1 -1
- package/dist/umd/index.js +7 -15
- package/dist/umd/index.js.br +0 -0
- package/dist/umd/index.js.gz +0 -0
- package/dist/umd/index.js.map +1 -1
- package/package.json +8 -8
- package/dist/public/js/emuiAppBridge.5a6bc2ea1f03ee954a75.js +0 -25
- package/dist/public/js/emuiAppBridge.5a6bc2ea1f03ee954a75.js.br +0 -0
- package/dist/public/js/emuiAppBridge.5a6bc2ea1f03ee954a75.js.gz +0 -0
- package/dist/public/js/emuiAppBridge.5a6bc2ea1f03ee954a75.js.map +0 -1
package/dist/cjs/appBridge.js
CHANGED
|
@@ -153,18 +153,19 @@ class CAppBridge {
|
|
|
153
153
|
};
|
|
154
154
|
/**
|
|
155
155
|
* add app to active app list
|
|
156
|
-
* @param
|
|
157
|
-
* @param
|
|
158
|
-
* @param
|
|
159
|
-
* @param
|
|
160
|
-
* @param
|
|
161
|
-
* @param
|
|
156
|
+
* @param options
|
|
157
|
+
* @param options.id - application id
|
|
158
|
+
* @param options.instanceId - unique instance id
|
|
159
|
+
* @param options.documentEle - iframe document
|
|
160
|
+
* @param options.elementIds - ids of injected script/style elements
|
|
161
|
+
* @param options.microFEHost - pre-created host reference for guest-initiated loading
|
|
162
162
|
*/
|
|
163
163
|
#addAppToActiveAppList = ({
|
|
164
164
|
id,
|
|
165
165
|
instanceId,
|
|
166
166
|
documentEle,
|
|
167
|
-
elementIds
|
|
167
|
+
elementIds,
|
|
168
|
+
microFEHost
|
|
168
169
|
}) => {
|
|
169
170
|
const app = this.#appRegistry.get({ id, instanceId });
|
|
170
171
|
if (!app) {
|
|
@@ -177,7 +178,8 @@ class CAppBridge {
|
|
|
177
178
|
guest: {
|
|
178
179
|
guestWindow: documentEle?.defaultView,
|
|
179
180
|
...app
|
|
180
|
-
}
|
|
181
|
+
},
|
|
182
|
+
microFEHost
|
|
181
183
|
});
|
|
182
184
|
};
|
|
183
185
|
#initApplication = async ({
|
|
@@ -189,7 +191,8 @@ class CAppBridge {
|
|
|
189
191
|
homeRoute,
|
|
190
192
|
initialRoute,
|
|
191
193
|
history,
|
|
192
|
-
theme
|
|
194
|
+
theme,
|
|
195
|
+
microFEHost
|
|
193
196
|
}) => {
|
|
194
197
|
const app = this.#appRegistry.get({ id, instanceId });
|
|
195
198
|
if (!app) {
|
|
@@ -201,10 +204,8 @@ class CAppBridge {
|
|
|
201
204
|
throw new Error(
|
|
202
205
|
`Application ${id} with instance id ${instanceId} doesn't expose init method`
|
|
203
206
|
);
|
|
204
|
-
const host = new import_microfeHost.CMicroFEHost({
|
|
205
|
-
guest: {
|
|
206
|
-
id
|
|
207
|
-
},
|
|
207
|
+
const host = microFEHost ?? new import_microfeHost.CMicroFEHost({
|
|
208
|
+
guest: { id },
|
|
208
209
|
version: this.#version,
|
|
209
210
|
containerId,
|
|
210
211
|
logger: this.#logger,
|
|
@@ -232,7 +233,7 @@ class CAppBridge {
|
|
|
232
233
|
`Application ${id} with instance id ${instanceId} is loading...`
|
|
233
234
|
);
|
|
234
235
|
let assets = files;
|
|
235
|
-
const manifest = await import_loaders.ManifestLoader.get(options);
|
|
236
|
+
const manifest = options.manifest ?? await import_loaders.ManifestLoader.get(options);
|
|
236
237
|
assets = import_loaders.ManifestLoader.getFullFileNameofAssets(manifest, files);
|
|
237
238
|
const cssAssets = assets.filter((fileName) => isCss(fileName));
|
|
238
239
|
const jsAssets = assets.filter((fileName) => !isCss(fileName));
|
|
@@ -250,9 +251,12 @@ class CAppBridge {
|
|
|
250
251
|
id,
|
|
251
252
|
instanceId,
|
|
252
253
|
documentEle,
|
|
253
|
-
elementIds: [...styleElementIds, ...scriptElementIds]
|
|
254
|
+
elementIds: [...styleElementIds, ...scriptElementIds],
|
|
255
|
+
microFEHost: options.microFEHost
|
|
254
256
|
});
|
|
255
|
-
|
|
257
|
+
if (!options.selfInitialize) {
|
|
258
|
+
await this.#initApplication(options);
|
|
259
|
+
}
|
|
256
260
|
this.#logger.audit(
|
|
257
261
|
`Application ${id} with instance id ${instanceId} loaded`
|
|
258
262
|
);
|
|
@@ -351,6 +355,56 @@ class CAppBridge {
|
|
|
351
355
|
});
|
|
352
356
|
}
|
|
353
357
|
};
|
|
358
|
+
/**
|
|
359
|
+
* If the guest's module SO exposes _setUnloadHandler (duck-typed),
|
|
360
|
+
* bind it to closeApp so the guest can trigger its own teardown.
|
|
361
|
+
* @param id
|
|
362
|
+
* @param instanceId
|
|
363
|
+
*/
|
|
364
|
+
#bindModuleUnload = (id, instanceId) => {
|
|
365
|
+
const moduleSO = this.#soManager.getObject(
|
|
366
|
+
import_pui_scripting_object.ScriptingObjectNames.Module,
|
|
367
|
+
{ id }
|
|
368
|
+
);
|
|
369
|
+
const so = moduleSO;
|
|
370
|
+
const handler = so?._setUnloadHandler;
|
|
371
|
+
if (typeof handler === "function") {
|
|
372
|
+
handler(
|
|
373
|
+
() => this.closeApp(instanceId)
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* Dispatch the module.unloading event and collect feedback.
|
|
379
|
+
* Returns true if unload is allowed, false if any listener vetoed.
|
|
380
|
+
* @param id
|
|
381
|
+
*/
|
|
382
|
+
#canUnload = async (id) => {
|
|
383
|
+
const moduleSO = this.#soManager.getObject(
|
|
384
|
+
import_pui_scripting_object.ScriptingObjectNames.Module,
|
|
385
|
+
{ id }
|
|
386
|
+
);
|
|
387
|
+
if (!moduleSO) return true;
|
|
388
|
+
try {
|
|
389
|
+
const feedback = await this.#eventManager.dispatchEvent(moduleSO, {
|
|
390
|
+
event: { id: "module.unloading", name: "unloading" },
|
|
391
|
+
eventParams: { moduleId: id },
|
|
392
|
+
eventOptions: { timeout: 1e3 }
|
|
393
|
+
});
|
|
394
|
+
if (Array.isArray(feedback) && feedback.some((v) => v === false)) {
|
|
395
|
+
this.#logger.info(
|
|
396
|
+
`Unload denied by guest ${id} via module.unloading event`
|
|
397
|
+
);
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
this.#logger.warn({
|
|
402
|
+
message: `Error dispatching module.unloading event for ${id}`,
|
|
403
|
+
exception: err
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
};
|
|
354
408
|
/**
|
|
355
409
|
* clear session management for the guest application
|
|
356
410
|
* @param instanceId unique instance id of the application
|
|
@@ -483,6 +537,7 @@ class CAppBridge {
|
|
|
483
537
|
return;
|
|
484
538
|
}
|
|
485
539
|
const { id } = app;
|
|
540
|
+
if (!await this.#canUnload(id)) return;
|
|
486
541
|
const appConfig = this.#microFEConfig.getConfigById(id);
|
|
487
542
|
if (!appConfig) {
|
|
488
543
|
this.#logger.warn(`Configuration for application ${id} is not found`);
|
|
@@ -543,14 +598,46 @@ class CAppBridge {
|
|
|
543
598
|
*/
|
|
544
599
|
getApps = () => [...this.#activeApps.values()].map((app) => app.guest);
|
|
545
600
|
/**
|
|
546
|
-
* Initialize appBridge
|
|
601
|
+
* Initialize appBridge.
|
|
602
|
+
* @param preloadedAppConfig - Already-parsed config object. When provided
|
|
603
|
+
* the bridge skips its own HTTP fetch of app.config.json.
|
|
547
604
|
*/
|
|
548
|
-
init = async () => {
|
|
549
|
-
|
|
605
|
+
init = async (preloadedAppConfig) => {
|
|
606
|
+
if (preloadedAppConfig) {
|
|
607
|
+
this.#appConfig.setPreloadedConfig(preloadedAppConfig);
|
|
608
|
+
} else {
|
|
609
|
+
await this.#appConfig.load();
|
|
610
|
+
}
|
|
550
611
|
this.#microFEConfig.init({
|
|
551
612
|
version: this.#version,
|
|
552
613
|
appConfig: this.#appConfig
|
|
553
614
|
});
|
|
615
|
+
this.#injectPreconnects();
|
|
616
|
+
};
|
|
617
|
+
/**
|
|
618
|
+
* Inject `<link rel="preconnect">` tags into the parent document for each
|
|
619
|
+
* unique micro-app origin. Called once at the end of {@link init} so that
|
|
620
|
+
* DNS/TCP/TLS handshakes start before any iframe is created.
|
|
621
|
+
*/
|
|
622
|
+
#injectPreconnects = () => {
|
|
623
|
+
const apps = this.#appConfig.get("microFrontendApps") ?? {};
|
|
624
|
+
const origins = /* @__PURE__ */ new Set();
|
|
625
|
+
Object.keys(apps).forEach((id) => {
|
|
626
|
+
const cfg = this.#microFEConfig.getConfigById(id);
|
|
627
|
+
if (cfg?.hostUrl) {
|
|
628
|
+
try {
|
|
629
|
+
origins.add(new URL(cfg.hostUrl).origin);
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
origins.forEach((origin) => {
|
|
635
|
+
const link = document.createElement("link");
|
|
636
|
+
link.rel = "preconnect";
|
|
637
|
+
link.href = origin;
|
|
638
|
+
link.crossOrigin = "anonymous";
|
|
639
|
+
document.head.appendChild(link);
|
|
640
|
+
});
|
|
554
641
|
};
|
|
555
642
|
/**
|
|
556
643
|
* Mount guest micro frontend application into DOM
|
|
@@ -571,10 +658,42 @@ class CAppBridge {
|
|
|
571
658
|
}
|
|
572
659
|
await this.#mountApp({ ...appConfig, instanceId });
|
|
573
660
|
};
|
|
661
|
+
/**
|
|
662
|
+
* Prepare the iframe: create a CMicroFEHost, expose it on window.emui,
|
|
663
|
+
* and inject a preconnect hint for the guest origin.
|
|
664
|
+
* @param frameDoc
|
|
665
|
+
* @param appConfig
|
|
666
|
+
* @param appConfig.hostUrl
|
|
667
|
+
* @param options
|
|
668
|
+
* @param options.id
|
|
669
|
+
* @param options.containerId
|
|
670
|
+
*/
|
|
671
|
+
#setupIframe = (frameDoc, appConfig, options) => {
|
|
672
|
+
const microFEHost = new import_microfeHost.CMicroFEHost({
|
|
673
|
+
guest: { id: options.id },
|
|
674
|
+
version: this.#version,
|
|
675
|
+
containerId: options.containerId,
|
|
676
|
+
logger: this.#logger,
|
|
677
|
+
soManager: this.#soManager,
|
|
678
|
+
eventManager: this.#eventManager
|
|
679
|
+
});
|
|
680
|
+
const iframeWindow = frameDoc.defaultView;
|
|
681
|
+
if (iframeWindow) {
|
|
682
|
+
iframeWindow.emui = iframeWindow.emui ?? {};
|
|
683
|
+
iframeWindow.emui.__host = microFEHost;
|
|
684
|
+
}
|
|
685
|
+
const preconnect = frameDoc.createElement("link");
|
|
686
|
+
preconnect.rel = "preconnect";
|
|
687
|
+
preconnect.href = appConfig.hostUrl;
|
|
688
|
+
preconnect.crossOrigin = "anonymous";
|
|
689
|
+
frameDoc.head.appendChild(preconnect);
|
|
690
|
+
return microFEHost;
|
|
691
|
+
};
|
|
574
692
|
/**
|
|
575
693
|
* Open guest micro frontend application
|
|
576
694
|
* @param {OpenAppParams} params - options to open guest application
|
|
577
695
|
*/
|
|
696
|
+
// eslint-disable-next-line max-statements
|
|
578
697
|
openApp = async (params) => {
|
|
579
698
|
const {
|
|
580
699
|
id,
|
|
@@ -583,7 +702,8 @@ class CAppBridge {
|
|
|
583
702
|
theme,
|
|
584
703
|
homeRoute,
|
|
585
704
|
initialRoute,
|
|
586
|
-
metadata
|
|
705
|
+
metadata,
|
|
706
|
+
selfInitialize
|
|
587
707
|
} = params;
|
|
588
708
|
const instanceId = (0, import_uuid.v4)();
|
|
589
709
|
if (metadata) this.#guestMetadata.set(id, metadata);
|
|
@@ -591,15 +711,15 @@ class CAppBridge {
|
|
|
591
711
|
if (!appConfig) {
|
|
592
712
|
throw new Error(`Application ${id} is not found in app config`);
|
|
593
713
|
}
|
|
714
|
+
const manifestPromise = import_loaders.ManifestLoader.get(appConfig);
|
|
715
|
+
manifestPromise.catch(() => {
|
|
716
|
+
});
|
|
594
717
|
const frameEle = await import_frame.Frame.create({
|
|
595
718
|
id,
|
|
596
719
|
instanceId,
|
|
597
720
|
manifestPath: appConfig.manifestPath,
|
|
598
721
|
hostUrl: appConfig.hostUrl,
|
|
599
|
-
options: {
|
|
600
|
-
title: appConfig.name,
|
|
601
|
-
...frameOptions
|
|
602
|
-
}
|
|
722
|
+
options: { title: appConfig.name, ...frameOptions }
|
|
603
723
|
});
|
|
604
724
|
if (!frameEle?.contentDocument)
|
|
605
725
|
throw new Error("unable to create iframe for the microapp");
|
|
@@ -609,6 +729,15 @@ class CAppBridge {
|
|
|
609
729
|
instanceId,
|
|
610
730
|
documentEle: frameEle.contentDocument
|
|
611
731
|
});
|
|
732
|
+
const microFEHost = this.#setupIframe(
|
|
733
|
+
frameEle.contentDocument,
|
|
734
|
+
appConfig,
|
|
735
|
+
{
|
|
736
|
+
id,
|
|
737
|
+
containerId: frameOptions?.containerId
|
|
738
|
+
}
|
|
739
|
+
);
|
|
740
|
+
const manifest = await manifestPromise;
|
|
612
741
|
await this.#loadApp({
|
|
613
742
|
instanceId,
|
|
614
743
|
history,
|
|
@@ -617,10 +746,16 @@ class CAppBridge {
|
|
|
617
746
|
containerId: frameOptions?.containerId,
|
|
618
747
|
...appConfig,
|
|
619
748
|
homeRoute: homeRoute ?? appConfig.homeRoute,
|
|
620
|
-
initialRoute
|
|
749
|
+
initialRoute,
|
|
750
|
+
microFEHost,
|
|
751
|
+
selfInitialize,
|
|
752
|
+
manifest
|
|
621
753
|
});
|
|
622
|
-
|
|
754
|
+
if (!selfInitialize) {
|
|
755
|
+
await this.#mountApp({ instanceId, ...appConfig });
|
|
756
|
+
}
|
|
623
757
|
this.#manageSession({ id, instanceId });
|
|
758
|
+
this.#bindModuleUnload(id, instanceId);
|
|
624
759
|
return instanceId;
|
|
625
760
|
} catch (err) {
|
|
626
761
|
this.#unloadApp({
|
|
@@ -654,6 +789,18 @@ class CAppBridge {
|
|
|
654
789
|
removeScriptingObject = (objectId, guestId) => {
|
|
655
790
|
this.#soManager.removeScriptingObject(objectId, guestId);
|
|
656
791
|
};
|
|
792
|
+
/**
|
|
793
|
+
* Pre-fetch the manifest for a guest application so that a subsequent
|
|
794
|
+
* {@link openApp} call can skip the manifest network round-trip.
|
|
795
|
+
* Safe to call multiple times — results are cached.
|
|
796
|
+
* @param id application id as defined in app.config.json
|
|
797
|
+
*/
|
|
798
|
+
warmUp = (id) => {
|
|
799
|
+
const appConfig = this.#microFEConfig.getConfigById(id);
|
|
800
|
+
if (!appConfig) return;
|
|
801
|
+
import_loaders.ManifestLoader.get(appConfig).catch(() => {
|
|
802
|
+
});
|
|
803
|
+
};
|
|
657
804
|
/**
|
|
658
805
|
* Unmount guest micro frontend application from DOM
|
|
659
806
|
* @param instanceId unique instance id of guest micro frontend application
|
package/dist/cjs/config/app.js
CHANGED
|
@@ -85,6 +85,14 @@ class CAppConfig {
|
|
|
85
85
|
* @returns true if key exists
|
|
86
86
|
*/
|
|
87
87
|
has = (key = "") => (0, import_has.default)(this.#gAppConfig, key);
|
|
88
|
+
/**
|
|
89
|
+
* Accept an already-parsed config object, skipping the HTTP fetch.
|
|
90
|
+
* Useful when the caller (e.g. pui-app-sdk) has already loaded the config.
|
|
91
|
+
* @param config
|
|
92
|
+
*/
|
|
93
|
+
setPreloadedConfig = (config) => {
|
|
94
|
+
this.#gAppConfig = config;
|
|
95
|
+
};
|
|
88
96
|
/**
|
|
89
97
|
* add version to the base url
|
|
90
98
|
* @returns versioned base url
|
package/dist/cjs/frame.html
CHANGED
|
@@ -6,27 +6,6 @@
|
|
|
6
6
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
7
7
|
<link rel="icon" href="/favicon.ico" />
|
|
8
8
|
<title>Application</title>
|
|
9
|
-
<script nonce="__CSP_NONCE__">
|
|
10
|
-
(function (i, s, o, g, r, a, m) {
|
|
11
|
-
i['GoogleAnalyticsObject'] = r;
|
|
12
|
-
(i[r] =
|
|
13
|
-
i[r] ||
|
|
14
|
-
function () {
|
|
15
|
-
(i[r].q = i[r].q || []).push(arguments);
|
|
16
|
-
}),
|
|
17
|
-
(i[r].l = 1 * new Date());
|
|
18
|
-
(a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]);
|
|
19
|
-
a.async = 1;
|
|
20
|
-
a.src = g;
|
|
21
|
-
m.parentNode.insertBefore(a, m);
|
|
22
|
-
})(
|
|
23
|
-
window,
|
|
24
|
-
document,
|
|
25
|
-
'script',
|
|
26
|
-
'https://www.google-analytics.com/analytics.js',
|
|
27
|
-
'ga',
|
|
28
|
-
);
|
|
29
|
-
</script>
|
|
30
9
|
<style nonce="__CSP_NONCE__">
|
|
31
10
|
.full-width {
|
|
32
11
|
width: 100%;
|
|
@@ -121,9 +121,11 @@ class ScriptLoader {
|
|
|
121
121
|
* This method:
|
|
122
122
|
* - Partitions assets into CDN (HTTP/HTTPS) and non-CDN (relative) arrays in a single pass
|
|
123
123
|
* - Creates modulepreload links for all assets for performance optimization
|
|
124
|
-
* -
|
|
125
|
-
*
|
|
126
|
-
* -
|
|
124
|
+
* - Appends all script elements to the DOM synchronously so the browser
|
|
125
|
+
* can download them in parallel
|
|
126
|
+
* - Execution order is preserved by the spec: type="module" scripts
|
|
127
|
+
* execute in document order; dynamic async=false scripts execute in
|
|
128
|
+
* insertion order
|
|
127
129
|
* @example
|
|
128
130
|
* ```typescript
|
|
129
131
|
* const scriptIds = await scriptLoader.load(
|
|
@@ -158,15 +160,13 @@ class ScriptLoader {
|
|
|
158
160
|
nonCdnAssets: []
|
|
159
161
|
}
|
|
160
162
|
);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
href,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
return [...cdnAssets, ...nonCdnAssets].map((asset) => asset.id);
|
|
163
|
+
const orderedAssets = [...cdnAssets, ...nonCdnAssets];
|
|
164
|
+
await Promise.all(
|
|
165
|
+
orderedAssets.map(
|
|
166
|
+
({ id, href }) => this.#loadScript({ id, href, documentEle, isESMModule })
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
return orderedAssets.map((asset) => asset.id);
|
|
170
170
|
};
|
|
171
171
|
/**
|
|
172
172
|
* Removes a script element from the document by its ID.
|
|
@@ -155,14 +155,6 @@ const getServerHandlers = () => {
|
|
|
155
155
|
return versionedHandlers;
|
|
156
156
|
};
|
|
157
157
|
const serverHandlers = [
|
|
158
|
-
import_msw.rest.get(
|
|
159
|
-
"https://www.google-analytics.com/analytics.js",
|
|
160
|
-
(req, res, ctx) => res(
|
|
161
|
-
ctx.status(200),
|
|
162
|
-
ctx.set("Content-Type", "application/javascript"),
|
|
163
|
-
ctx.body("")
|
|
164
|
-
)
|
|
165
|
-
),
|
|
166
158
|
import_msw.rest.get(
|
|
167
159
|
"/latest/app.config.json",
|
|
168
160
|
(req, res, ctx) => res(ctx.json(import_app_config.default))
|