@elliemae/microfe-common 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/app.js +33 -0
- package/dist/cjs/auditThrottler.js +60 -0
- package/dist/cjs/config.js +65 -0
- package/dist/cjs/frame.html +47 -0
- package/dist/cjs/frame.js +61 -0
- package/dist/cjs/history.js +27 -0
- package/dist/cjs/host.js +105 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/loader.js +158 -0
- package/dist/cjs/logRecords.js +57 -0
- package/dist/cjs/logger.js +34 -0
- package/dist/cjs/manifestLoader.js +66 -0
- package/dist/cjs/scriptLoader.js +71 -0
- package/dist/cjs/styleLoader.js +57 -0
- package/dist/cjs/tests/loan/latest/index.js +80 -0
- package/dist/cjs/tests/loan/latest/manifest.json +3 -0
- package/dist/cjs/tests/server.js +29 -0
- package/dist/cjs/tests/serverHandlers.js +81 -0
- package/dist/cjs/tests/task/latest/index.js +58 -0
- package/dist/cjs/tests/task/latest/manifest.json +3 -0
- package/dist/cjs/typings/guest.js +16 -0
- package/dist/cjs/typings/host.js +16 -0
- package/dist/cjs/typings/index.js +16 -0
- package/dist/cjs/typings/microapp.js +16 -0
- package/dist/cjs/utils.js +33 -0
- package/dist/cjs/window.js +63 -0
- package/dist/esm/app.js +13 -0
- package/dist/esm/auditThrottler.js +40 -0
- package/dist/esm/config.js +35 -0
- package/dist/esm/frame.html +47 -0
- package/dist/esm/frame.js +41 -0
- package/dist/esm/history.js +7 -0
- package/dist/esm/host.js +85 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/loader.js +145 -0
- package/dist/esm/logRecords.js +37 -0
- package/dist/esm/logger.js +17 -0
- package/dist/esm/manifestLoader.js +46 -0
- package/dist/esm/scriptLoader.js +51 -0
- package/dist/esm/styleLoader.js +37 -0
- package/dist/esm/tests/loan/latest/index.js +79 -0
- package/dist/esm/tests/loan/latest/manifest.json +3 -0
- package/dist/esm/tests/server.js +9 -0
- package/dist/esm/tests/serverHandlers.js +51 -0
- package/dist/esm/tests/task/latest/index.js +57 -0
- package/dist/esm/tests/task/latest/manifest.json +3 -0
- package/dist/esm/typings/guest.js +0 -0
- package/dist/esm/typings/host.js +0 -0
- package/dist/esm/typings/index.js +0 -0
- package/dist/esm/typings/microapp.js +0 -0
- package/dist/esm/utils.js +13 -0
- package/dist/esm/window.js +43 -0
- package/dist/types/lib/app.d.ts +2 -0
- package/dist/types/lib/auditThrottler.d.ts +29 -0
- package/dist/types/lib/config.d.ts +25 -0
- package/dist/types/lib/frame.d.ts +10 -0
- package/dist/types/lib/history.d.ts +2 -0
- package/dist/types/lib/host.d.ts +42 -0
- package/dist/types/lib/index.d.ts +2 -0
- package/dist/types/lib/loader.d.ts +9 -0
- package/dist/types/lib/logRecords.d.ts +14 -0
- package/dist/types/lib/logger.d.ts +1 -0
- package/dist/types/lib/manifestLoader.d.ts +7 -0
- package/dist/types/lib/scriptLoader.d.ts +6 -0
- package/dist/types/lib/styleLoader.d.ts +5 -0
- package/dist/types/lib/tests/auditThrottler.test.d.ts +1 -0
- package/dist/types/lib/tests/loader.test.d.ts +1 -0
- package/dist/types/lib/tests/loan/latest/index.d.ts +0 -0
- package/dist/types/lib/tests/server.d.ts +1 -0
- package/dist/types/lib/tests/serverHandlers.d.ts +1 -0
- package/dist/types/lib/tests/task/latest/index.d.ts +0 -0
- package/dist/types/lib/typings/guest.d.ts +67 -0
- package/dist/types/lib/typings/host.d.ts +60 -0
- package/dist/types/lib/typings/index.d.ts +3 -0
- package/dist/types/lib/typings/microapp.d.ts +17 -0
- package/dist/types/lib/utils.d.ts +3 -0
- package/dist/types/lib/window.d.ts +27 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -3
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class AuditThrottler {
|
|
2
|
+
#logger;
|
|
3
|
+
#enabled;
|
|
4
|
+
#throttleMs;
|
|
5
|
+
#lastAudited = /* @__PURE__ */ new Map();
|
|
6
|
+
static DEFAULT_THROTTLE_MS = 1e4;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.#logger = options.logger;
|
|
9
|
+
this.#enabled = options.enabled ?? true;
|
|
10
|
+
this.#throttleMs = options.throttleMs ?? AuditThrottler.DEFAULT_THROTTLE_MS;
|
|
11
|
+
}
|
|
12
|
+
get enabled() {
|
|
13
|
+
return this.#enabled;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Log a high-frequency operation. When audit is enabled and the throttle
|
|
17
|
+
* window allows it the payload is sent at `audit` level; otherwise `debug`.
|
|
18
|
+
* @param {string} key - dedup key for throttling (e.g. `invoke:loan.getField`)
|
|
19
|
+
* @param {LogRecord} payload - structured log payload
|
|
20
|
+
*/
|
|
21
|
+
log(key, payload) {
|
|
22
|
+
if (!this.#enabled) {
|
|
23
|
+
this.#logger.debug(payload);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (this.#throttleMs > 0) {
|
|
27
|
+
const now = performance.now();
|
|
28
|
+
const last = this.#lastAudited.get(key);
|
|
29
|
+
if (last !== void 0 && now - last < this.#throttleMs) {
|
|
30
|
+
this.#logger.debug(payload);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.#lastAudited.set(key, now);
|
|
34
|
+
}
|
|
35
|
+
this.#logger.audit(payload);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
AuditThrottler
|
|
40
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
import appConfig from "./app.config.json";
|
|
3
|
+
import { getAssetPath } from "./window.js";
|
|
4
|
+
let gAppConfig = appConfig;
|
|
5
|
+
const setAppConfig = (config) => {
|
|
6
|
+
gAppConfig = config;
|
|
7
|
+
};
|
|
8
|
+
const getAppConfigValue = (key = "", defaultValue = null) => _.clone(_.get(gAppConfig, key, defaultValue));
|
|
9
|
+
const setAppConfigValue = (key, value) => _.set(gAppConfig, key, value);
|
|
10
|
+
const hasItem = (key = "") => _.has(gAppConfig, key);
|
|
11
|
+
const parseAppConfig = (data) => {
|
|
12
|
+
const { activeEnv } = data;
|
|
13
|
+
const activeEnvConfig = data.env[activeEnv] || {};
|
|
14
|
+
if (data.env) delete data.env;
|
|
15
|
+
setAppConfig(_.merge(data, activeEnvConfig));
|
|
16
|
+
};
|
|
17
|
+
const loadAppConfig = (assetPath = "") => new Promise((resolve, reject) => {
|
|
18
|
+
fetch(`${assetPath || getAssetPath()}app.config.json`).then((response) => response.json()).then(({ data }) => {
|
|
19
|
+
parseAppConfig(data);
|
|
20
|
+
resolve();
|
|
21
|
+
}).catch((err) => {
|
|
22
|
+
reject(
|
|
23
|
+
new Error(
|
|
24
|
+
`Unable to load application configurtion file. ${err.Message}`
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
export {
|
|
30
|
+
getAppConfigValue,
|
|
31
|
+
hasItem,
|
|
32
|
+
loadAppConfig,
|
|
33
|
+
setAppConfig,
|
|
34
|
+
setAppConfigValue
|
|
35
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
7
|
+
<link rel="icon" href="/favicon.ico" />
|
|
8
|
+
<title>Application</title>
|
|
9
|
+
<script>
|
|
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
|
+
<style>
|
|
31
|
+
.full-width {
|
|
32
|
+
width: 100%;
|
|
33
|
+
}
|
|
34
|
+
.full-height {
|
|
35
|
+
height: 100%;
|
|
36
|
+
}
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body class="full-width full-height">
|
|
40
|
+
<noscript
|
|
41
|
+
>If you're seeing this message, that means
|
|
42
|
+
<strong>JavaScript has been disabled on your browser</strong>, please
|
|
43
|
+
<strong>enable JS</strong> to make this app work.</noscript
|
|
44
|
+
>
|
|
45
|
+
<div id="pui-app-container-" class="full-width full-height"></div>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const FRAME_CONTAINER_ID_PREFIX = "pui-iframe-container-";
|
|
2
|
+
const FRAME_APP_CONTAINER_ID_PREFIX = "pui-app-container-";
|
|
3
|
+
const createFrame = (options) => new Promise((resolve, reject) => {
|
|
4
|
+
const parentContainer = document.createElement("div");
|
|
5
|
+
parentContainer.setAttribute(
|
|
6
|
+
"style",
|
|
7
|
+
"display: flex;width: 100%;height: 100%;flex-direction: column;overflow: hidden;"
|
|
8
|
+
);
|
|
9
|
+
const frame = document.createElement("iframe");
|
|
10
|
+
frame.setAttribute("id", `${FRAME_CONTAINER_ID_PREFIX}${options.id}`);
|
|
11
|
+
frame.setAttribute("frameborder", "0");
|
|
12
|
+
frame.setAttribute("scrolling", "no");
|
|
13
|
+
frame.setAttribute("allowfullscreen", "true");
|
|
14
|
+
frame.setAttribute("allowtransparency", "true");
|
|
15
|
+
frame.setAttribute("allow", "microphone; camera");
|
|
16
|
+
if (options.sandbox) frame.setAttribute("sandbox", options.sandbox);
|
|
17
|
+
frame.setAttribute(
|
|
18
|
+
"style",
|
|
19
|
+
options.style ?? "flex-grow: 1;border: none;margin: 0;padding: 0;display: block;height: 100%;"
|
|
20
|
+
);
|
|
21
|
+
frame.setAttribute("src", options.src ?? "./frame.html");
|
|
22
|
+
frame.addEventListener("load", () => {
|
|
23
|
+
if (!frame.contentDocument) {
|
|
24
|
+
reject(new Error("Frame content window is null"));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const documentEle = frame.contentDocument;
|
|
28
|
+
const ele = documentEle.getElementById(FRAME_APP_CONTAINER_ID_PREFIX);
|
|
29
|
+
if (ele) {
|
|
30
|
+
ele.id = `${ele.id}${options.id}`;
|
|
31
|
+
}
|
|
32
|
+
resolve(frame);
|
|
33
|
+
});
|
|
34
|
+
parentContainer.appendChild(frame);
|
|
35
|
+
document.body.appendChild(parentContainer);
|
|
36
|
+
});
|
|
37
|
+
export {
|
|
38
|
+
FRAME_APP_CONTAINER_ID_PREFIX,
|
|
39
|
+
FRAME_CONTAINER_ID_PREFIX,
|
|
40
|
+
createFrame
|
|
41
|
+
};
|
package/dist/esm/host.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { publish, subscribe, unsubscribe } from "pubsub-js";
|
|
2
|
+
import { getDefaultTheme } from "@elliemae/pui-theme";
|
|
3
|
+
import { FRAME_CONTAINER_ID_PREFIX } from "./frame.js";
|
|
4
|
+
import { getAppConfigValue, loadAppConfig } from "./config.js";
|
|
5
|
+
import { browserHistory } from "./history.js";
|
|
6
|
+
import { logger } from "./logger.js";
|
|
7
|
+
class CMicroAppHost {
|
|
8
|
+
static instance;
|
|
9
|
+
logger;
|
|
10
|
+
appId;
|
|
11
|
+
props;
|
|
12
|
+
activeGuests;
|
|
13
|
+
onInit;
|
|
14
|
+
onRenewSessionTimer;
|
|
15
|
+
scriptingObjects;
|
|
16
|
+
constructor(params) {
|
|
17
|
+
this.appId = getAppConfigValue("appId");
|
|
18
|
+
this.onInit = params?.onInit;
|
|
19
|
+
this.logger = params?.logger || logger;
|
|
20
|
+
this.onRenewSessionTimer = params?.onRenewSessionTimer;
|
|
21
|
+
this.props = {
|
|
22
|
+
systemVersion: params?.version ?? "latest",
|
|
23
|
+
history: params?.history ?? browserHistory,
|
|
24
|
+
theme: params?.theme ?? getDefaultTheme()
|
|
25
|
+
};
|
|
26
|
+
this.activeGuests = {};
|
|
27
|
+
this.scriptingObjects = params?.scriptingObjects ?? {};
|
|
28
|
+
this.getProps = this.getProps.bind(this);
|
|
29
|
+
this.getLogger = this.getLogger.bind(this);
|
|
30
|
+
this.getGuests = this.getGuests.bind(this);
|
|
31
|
+
this.getGuest = this.getGuest.bind(this);
|
|
32
|
+
this.getObject = this.getObject.bind(this);
|
|
33
|
+
loadAppConfig().then(() => {
|
|
34
|
+
if (this.onInit) this.onInit(this.props);
|
|
35
|
+
}).catch(() => {
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
static getInstance(params) {
|
|
39
|
+
if (!this.instance) {
|
|
40
|
+
this.instance = new this(params);
|
|
41
|
+
}
|
|
42
|
+
return this.instance;
|
|
43
|
+
}
|
|
44
|
+
static isInitialized() {
|
|
45
|
+
return !!this.instance;
|
|
46
|
+
}
|
|
47
|
+
getProps() {
|
|
48
|
+
return this.props;
|
|
49
|
+
}
|
|
50
|
+
getLogger() {
|
|
51
|
+
return this.logger;
|
|
52
|
+
}
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
54
|
+
async getObject(name) {
|
|
55
|
+
return this.scriptingObjects[name];
|
|
56
|
+
}
|
|
57
|
+
getGuests() {
|
|
58
|
+
return this.activeGuests;
|
|
59
|
+
}
|
|
60
|
+
getGuest(id) {
|
|
61
|
+
return this.activeGuests[id] ? this.activeGuests[id] : null;
|
|
62
|
+
}
|
|
63
|
+
setAppWindowSize(appSize) {
|
|
64
|
+
const { appId, size } = appSize;
|
|
65
|
+
const frameEle = document.getElementById(
|
|
66
|
+
`${FRAME_CONTAINER_ID_PREFIX}${appId}`
|
|
67
|
+
);
|
|
68
|
+
if (frameEle) {
|
|
69
|
+
frameEle.style.height = `${size.height}px`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
publish(eventId, data) {
|
|
73
|
+
return publish(eventId, data);
|
|
74
|
+
}
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
76
|
+
subscribe(eventId, listener) {
|
|
77
|
+
return subscribe(eventId, listener);
|
|
78
|
+
}
|
|
79
|
+
unsubscribe(token) {
|
|
80
|
+
unsubscribe(token);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
CMicroAppHost
|
|
85
|
+
};
|
package/dist/esm/index.js
CHANGED
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
SecurityContext
|
|
9
9
|
} from "./scriptingObjectManager.js";
|
|
10
10
|
import { ScriptingObjectProxy, isScriptingObjectProxy } from "./proxy.js";
|
|
11
|
+
import { AuditThrottler } from "./auditThrottler.js";
|
|
11
12
|
export {
|
|
13
|
+
AuditThrottler,
|
|
12
14
|
Event,
|
|
13
15
|
MessageType,
|
|
14
16
|
ProxyEvent,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { CMicroAppHost } from "./host.js";
|
|
2
|
+
import { getAbsoluteUrl } from "./utils.js";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
import { logRecords } from "./logRecords.js";
|
|
5
|
+
import { getCurrentBreakpoint, getViewportSize } from "./window.js";
|
|
6
|
+
import { addStylesToDOM, removeDynamicImportedStyles } from "./styleLoader.js";
|
|
7
|
+
import {
|
|
8
|
+
addScriptToDOM,
|
|
9
|
+
removeDynamicImportedScripts,
|
|
10
|
+
removePrefetchLinks
|
|
11
|
+
} from "./scriptLoader.js";
|
|
12
|
+
import {
|
|
13
|
+
getAppManifest,
|
|
14
|
+
getFullFileNameofAssetsFromManifest
|
|
15
|
+
} from "./manifestLoader.js";
|
|
16
|
+
const APP_CONTAINER_ID_PREFIX = "pui-app-container-";
|
|
17
|
+
const cssType = /\.css$/;
|
|
18
|
+
const isCss = (fileName) => cssType.test(fileName);
|
|
19
|
+
const activeApps = {};
|
|
20
|
+
const getPathName = (url) => url ? new URL(url).pathname : "";
|
|
21
|
+
const initApplication = async ({
|
|
22
|
+
id,
|
|
23
|
+
name,
|
|
24
|
+
hostUrl,
|
|
25
|
+
history,
|
|
26
|
+
theme,
|
|
27
|
+
manifestPath,
|
|
28
|
+
homeRoute
|
|
29
|
+
}) => {
|
|
30
|
+
const app = window.emui?.[id];
|
|
31
|
+
if (!app) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Application ${name} with ${id} is not found. Most probably the appId property of app.config.json is not set to ${id}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const { init } = app;
|
|
37
|
+
if (!init || typeof init !== "function")
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Application ${name} with id ${id} doesn't expose init method.`
|
|
40
|
+
);
|
|
41
|
+
return init({
|
|
42
|
+
host: CMicroAppHost.getInstance(),
|
|
43
|
+
hostUrl,
|
|
44
|
+
manifestPath,
|
|
45
|
+
homeRoute: homeRoute ?? getPathName(hostUrl),
|
|
46
|
+
prevState: null,
|
|
47
|
+
history,
|
|
48
|
+
theme,
|
|
49
|
+
hostBreakpoint: getCurrentBreakpoint(),
|
|
50
|
+
hostViewportSize: getViewportSize(),
|
|
51
|
+
logger
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const mountApp = async ({ id, name }) => {
|
|
55
|
+
const app = (window.emui || {})[id] || {};
|
|
56
|
+
const { mount } = app;
|
|
57
|
+
if (!mount || typeof mount !== "function")
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Application ${name} with id ${id} doesn't expose mount method.`
|
|
60
|
+
);
|
|
61
|
+
return mount({
|
|
62
|
+
containerId: `${APP_CONTAINER_ID_PREFIX}${id}`,
|
|
63
|
+
hostBreakpoint: getCurrentBreakpoint(),
|
|
64
|
+
hostViewportSize: getViewportSize()
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const unmountApp = async ({ id, name }) => {
|
|
68
|
+
const app = (window.emui || {})[id];
|
|
69
|
+
if (!app) return null;
|
|
70
|
+
const { unmount } = app;
|
|
71
|
+
if (!unmount) return null;
|
|
72
|
+
if (typeof unmount !== "function")
|
|
73
|
+
throw new Error(
|
|
74
|
+
`unmount failed for application ${name} with id ${id}. unmount is not a valid function`
|
|
75
|
+
);
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
};
|
|
78
|
+
const addAppToActiveAppList = (id, elementIds) => {
|
|
79
|
+
const app = (window.emui || {})[id] || {};
|
|
80
|
+
const { getObject } = app;
|
|
81
|
+
activeApps[id] = { elementIds };
|
|
82
|
+
const host = CMicroAppHost.getInstance();
|
|
83
|
+
if (host) host.activeGuests[id] = getObject && getObject() || {};
|
|
84
|
+
return Promise.resolve();
|
|
85
|
+
};
|
|
86
|
+
const waitAndInitApplication = (appConfig, requests) => (
|
|
87
|
+
// wait for all assets to get downloaded
|
|
88
|
+
Promise.all(requests).then(addAppToActiveAppList.bind(null, appConfig.id)).then(initApplication.bind(null, appConfig)).catch((err) => {
|
|
89
|
+
const logRecord = logRecords.APP_INIT_FAILED(appConfig.id, err.message);
|
|
90
|
+
logger.error({ ...logRecord, exception: err });
|
|
91
|
+
throw new Error(logRecord.message);
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
const removeAssetsFromDOM = (id, documentEle = document) => {
|
|
95
|
+
const host = CMicroAppHost.getInstance();
|
|
96
|
+
if (host) delete host.activeGuests[id];
|
|
97
|
+
const { elementIds } = activeApps[id] || {};
|
|
98
|
+
if (elementIds) {
|
|
99
|
+
elementIds.forEach((elementId) => {
|
|
100
|
+
const ele = documentEle.getElementById(elementId);
|
|
101
|
+
if (ele) ele.remove();
|
|
102
|
+
});
|
|
103
|
+
delete activeApps[id];
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const sanitizeAppConfig = (appConfig) => {
|
|
107
|
+
const { hostUrl = "" } = appConfig;
|
|
108
|
+
appConfig.hostUrl = getAbsoluteUrl(hostUrl).replace(/\/?$/, "/");
|
|
109
|
+
};
|
|
110
|
+
const loadApp = async (appConfig) => {
|
|
111
|
+
logger.info(logRecords.APP_LOADING(appConfig.id));
|
|
112
|
+
sanitizeAppConfig(appConfig);
|
|
113
|
+
let assets = appConfig.files;
|
|
114
|
+
const manifest = await getAppManifest(appConfig);
|
|
115
|
+
assets = getFullFileNameofAssetsFromManifest(manifest, appConfig.files);
|
|
116
|
+
let counter = 0;
|
|
117
|
+
const requests = assets.map((fileName) => {
|
|
118
|
+
counter += 1;
|
|
119
|
+
return !isCss(fileName) ? addScriptToDOM(appConfig, fileName, counter) : addStylesToDOM(appConfig, fileName, counter);
|
|
120
|
+
});
|
|
121
|
+
await waitAndInitApplication(appConfig, requests);
|
|
122
|
+
logger.info(logRecords.APP_LOADING_COMPLETE(appConfig.id));
|
|
123
|
+
};
|
|
124
|
+
const unloadApp = ({
|
|
125
|
+
id,
|
|
126
|
+
hostUrl,
|
|
127
|
+
documentEle
|
|
128
|
+
}) => {
|
|
129
|
+
if (!hostUrl) throw new Error("Unable to unload app. hostUrl is required");
|
|
130
|
+
logger.info(logRecords.APP_UNLOADING(id));
|
|
131
|
+
const app = (window.emui || {})[id];
|
|
132
|
+
if (!app) return;
|
|
133
|
+
removeAssetsFromDOM(id, documentEle);
|
|
134
|
+
removeDynamicImportedScripts(hostUrl, documentEle);
|
|
135
|
+
removePrefetchLinks(hostUrl, documentEle);
|
|
136
|
+
removeDynamicImportedStyles(hostUrl, documentEle);
|
|
137
|
+
if (window.emui && window.emui[id]) delete window.emui[id];
|
|
138
|
+
logger.info(logRecords.APP_UNLOADING_COMPLETE(id));
|
|
139
|
+
};
|
|
140
|
+
export {
|
|
141
|
+
loadApp,
|
|
142
|
+
mountApp,
|
|
143
|
+
unloadApp,
|
|
144
|
+
unmountApp
|
|
145
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const logRecords = {
|
|
2
|
+
APP_CONFIG_LOAD_FAILED: {
|
|
3
|
+
code: "microfe01",
|
|
4
|
+
message: "Unable to load application configuration"
|
|
5
|
+
},
|
|
6
|
+
ASSET_NOT_FOUND_IN_MANIFEST: (assetName) => ({
|
|
7
|
+
code: "microfe02",
|
|
8
|
+
message: `Application load failed. unable to locate ${assetName} in the manifest`
|
|
9
|
+
}),
|
|
10
|
+
APP_INIT_FAILED: (appId, errMsg) => ({
|
|
11
|
+
code: "microfe03",
|
|
12
|
+
message: `Application load failed. Unable to load one or more application resources for appId: ${appId}. ${errMsg}`
|
|
13
|
+
}),
|
|
14
|
+
APP_LOADING: (appId) => ({
|
|
15
|
+
code: "microfe04",
|
|
16
|
+
message: `Application ${appId} is loading...`
|
|
17
|
+
}),
|
|
18
|
+
APP_LOADING_COMPLETE: (appId) => ({
|
|
19
|
+
code: "microfe05",
|
|
20
|
+
message: `Application ${appId} loaded`
|
|
21
|
+
}),
|
|
22
|
+
APP_UNLOADING: (appId) => ({
|
|
23
|
+
code: "microfe06",
|
|
24
|
+
message: `Application ${appId} unloading...`
|
|
25
|
+
}),
|
|
26
|
+
APP_UNLOADING_COMPLETE: (appId) => ({
|
|
27
|
+
code: "microfe07",
|
|
28
|
+
message: `Application ${appId} unloaded`
|
|
29
|
+
}),
|
|
30
|
+
SSF_HOST_OBJECT_NOT_FOUND: (name) => ({
|
|
31
|
+
code: "microfe08",
|
|
32
|
+
message: `Parent window doesn't expose SSF Object named ${name}`
|
|
33
|
+
})
|
|
34
|
+
};
|
|
35
|
+
export {
|
|
36
|
+
logRecords
|
|
37
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logger as uiLogger,
|
|
3
|
+
Console
|
|
4
|
+
} from "@elliemae/pui-diagnostics";
|
|
5
|
+
const consoleLogger = (() => uiLogger({
|
|
6
|
+
transport: Console(),
|
|
7
|
+
index: "app",
|
|
8
|
+
environment: "NA",
|
|
9
|
+
team: "app team",
|
|
10
|
+
appName: "ICEMT App",
|
|
11
|
+
appVersion: "latest",
|
|
12
|
+
userId: ""
|
|
13
|
+
}))();
|
|
14
|
+
const logger = window?.emui?.logger || consoleLogger;
|
|
15
|
+
export {
|
|
16
|
+
logger
|
|
17
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { removeDoubleSlash } from "./utils.js";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
import { logRecords } from "./logRecords.js";
|
|
4
|
+
const getUnVersionedManifestPath = (path) => path.replace(/\/\d+\.\d+/, "/latest");
|
|
5
|
+
const getAppManifest = async ({
|
|
6
|
+
hostUrl,
|
|
7
|
+
manifestPath
|
|
8
|
+
}) => {
|
|
9
|
+
if (!hostUrl || !manifestPath)
|
|
10
|
+
throw new Error(
|
|
11
|
+
"Unable to get app manifest. hostUrl and manifestPath are required."
|
|
12
|
+
);
|
|
13
|
+
const url = new URL(
|
|
14
|
+
`${manifestPath.replace(/\/?$/, "/")}manifest.json`,
|
|
15
|
+
hostUrl
|
|
16
|
+
);
|
|
17
|
+
const response = await fetch(removeDoubleSlash(url.href));
|
|
18
|
+
const { headers } = response;
|
|
19
|
+
const contentType = headers?.get?.("content-type") ?? "";
|
|
20
|
+
if (contentType.includes("application/json")) {
|
|
21
|
+
const data = await response.json();
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
const unVersionedManifestPath = getUnVersionedManifestPath(manifestPath);
|
|
25
|
+
if (manifestPath !== unVersionedManifestPath) {
|
|
26
|
+
return getAppManifest({
|
|
27
|
+
hostUrl,
|
|
28
|
+
manifestPath: getUnVersionedManifestPath(manifestPath)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
throw new Error("manifest.json is not available for the application");
|
|
32
|
+
};
|
|
33
|
+
const getFullFileNameofAssetsFromManifest = (manifest, assetNames = []) => assetNames.reduce((assets, assetName) => {
|
|
34
|
+
const fullFileName = manifest[assetName];
|
|
35
|
+
if (fullFileName) assets.push(fullFileName);
|
|
36
|
+
else {
|
|
37
|
+
const logRecord = logRecords.ASSET_NOT_FOUND_IN_MANIFEST(assetName);
|
|
38
|
+
logger.error(logRecord);
|
|
39
|
+
throw new Error(logRecord.message);
|
|
40
|
+
}
|
|
41
|
+
return assets;
|
|
42
|
+
}, []);
|
|
43
|
+
export {
|
|
44
|
+
getAppManifest,
|
|
45
|
+
getFullFileNameofAssetsFromManifest
|
|
46
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { removeDoubleSlash } from "./utils.js";
|
|
2
|
+
const APP_SCRIPT_ID_PREFIX = "emui-script-";
|
|
3
|
+
const HEAD_SCRIPTS = /(?:emuiDiagnostics|global|global-prod|emuiUserMonitoring)(?:..*)?.js/;
|
|
4
|
+
const isHeadScript = (scriptSrc) => HEAD_SCRIPTS.test(scriptSrc);
|
|
5
|
+
const addScriptToDOM = ({ name, hostUrl, documentEle }, fileName, index) => {
|
|
6
|
+
if (!hostUrl)
|
|
7
|
+
throw new Error("Unable to add scripts to DOM. hostUrl is required.");
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const ele = documentEle.createElement("script");
|
|
10
|
+
if (!ele) reject(new Error("Unable to insert Application scripts."));
|
|
11
|
+
ele.id = `${APP_SCRIPT_ID_PREFIX}${name}-${index}`;
|
|
12
|
+
const url = new URL(fileName, hostUrl);
|
|
13
|
+
ele.src = removeDoubleSlash(url.href);
|
|
14
|
+
ele.onload = resolve.bind(null, ele.id);
|
|
15
|
+
ele.onerror = reject.bind(null, ele.id);
|
|
16
|
+
ele.async = false;
|
|
17
|
+
if (isHeadScript(ele.src)) documentEle.head.appendChild(ele);
|
|
18
|
+
else documentEle.body.appendChild(ele);
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const removeScriptFromDOM = (elementId = "", documentEle = document) => new Promise((resolve) => {
|
|
22
|
+
const ele = documentEle.getElementById(elementId);
|
|
23
|
+
if (!ele) console.warn(new Error(`script with id ${elementId} not found`));
|
|
24
|
+
ele.remove();
|
|
25
|
+
resolve();
|
|
26
|
+
});
|
|
27
|
+
const removeDynamicImportedScripts = (hostUrl, documentEle) => {
|
|
28
|
+
const hostPattern = new RegExp(hostUrl, "i");
|
|
29
|
+
const scriptElements = documentEle.getElementsByTagName("script");
|
|
30
|
+
for (let index = scriptElements.length - 1; index >= 0; index -= 1) {
|
|
31
|
+
const scriptEle = scriptElements[index];
|
|
32
|
+
const { src } = scriptEle;
|
|
33
|
+
if (hostPattern.test(src)) scriptEle.remove();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const removePrefetchLinks = (hostUrl, documentEle) => {
|
|
37
|
+
const hostPattern = new RegExp(hostUrl, "i");
|
|
38
|
+
const prefetchElements = documentEle.querySelectorAll('[rel="prefetch"]');
|
|
39
|
+
for (let index = prefetchElements.length - 1; index >= 0; index -= 1) {
|
|
40
|
+
const ele = prefetchElements[index];
|
|
41
|
+
const { href } = ele;
|
|
42
|
+
if (hostPattern.test(href)) ele.remove();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
export {
|
|
46
|
+
APP_SCRIPT_ID_PREFIX,
|
|
47
|
+
addScriptToDOM,
|
|
48
|
+
removeDynamicImportedScripts,
|
|
49
|
+
removePrefetchLinks,
|
|
50
|
+
removeScriptFromDOM
|
|
51
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { removeDoubleSlash } from "./utils.js";
|
|
2
|
+
const APP_STYLE_ID_PREFIX = "emui-style-";
|
|
3
|
+
const addStylesToDOM = ({ name, hostUrl, documentEle }, fileName, index) => {
|
|
4
|
+
if (!hostUrl)
|
|
5
|
+
throw new Error("Unable to add styles to DOM. hostUrl is required.");
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const ele = documentEle.createElement("link");
|
|
8
|
+
if (!ele) reject(new Error("Unable to insert Application styles."));
|
|
9
|
+
ele.id = `${APP_STYLE_ID_PREFIX}${name}-${index}`;
|
|
10
|
+
ele.rel = "stylesheet";
|
|
11
|
+
const url = new URL(fileName, hostUrl);
|
|
12
|
+
ele.href = removeDoubleSlash(url.href);
|
|
13
|
+
ele.onload = resolve.bind(null, ele.id);
|
|
14
|
+
documentEle.head.appendChild(ele);
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
const removeStyleFromDOM = (elementId = "", documentEle = document) => new Promise((resolve) => {
|
|
18
|
+
const ele = documentEle.getElementById(elementId);
|
|
19
|
+
if (!ele) console.warn(new Error(`style with id ${elementId} not found`));
|
|
20
|
+
ele.remove();
|
|
21
|
+
resolve();
|
|
22
|
+
});
|
|
23
|
+
const removeDynamicImportedStyles = (hostUrl, documentEle) => {
|
|
24
|
+
const hostPattern = new RegExp(hostUrl, "i");
|
|
25
|
+
const prefetchElements = documentEle.querySelectorAll('[rel="stylesheet"]');
|
|
26
|
+
for (let index = prefetchElements.length - 1; index >= 0; index -= 1) {
|
|
27
|
+
const ele = prefetchElements[index];
|
|
28
|
+
const { href } = ele;
|
|
29
|
+
if (hostPattern.test(href)) ele.remove();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export {
|
|
33
|
+
APP_STYLE_ID_PREFIX,
|
|
34
|
+
addStylesToDOM,
|
|
35
|
+
removeDynamicImportedStyles,
|
|
36
|
+
removeStyleFromDOM
|
|
37
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
const appId = "loanapp";
|
|
3
|
+
const appName = "Loan App";
|
|
4
|
+
const appElementId = `pui-app-container-${appId}`;
|
|
5
|
+
const pipelinePath = "/pipeline";
|
|
6
|
+
const pipelineLinkText = "Pipeline";
|
|
7
|
+
const getWindow = () => {
|
|
8
|
+
try {
|
|
9
|
+
window.parent.document;
|
|
10
|
+
return window.parent;
|
|
11
|
+
} catch (err) {
|
|
12
|
+
return window;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
let host = null;
|
|
16
|
+
let parentHistory = null;
|
|
17
|
+
let logger = null;
|
|
18
|
+
const browserWindow = getWindow();
|
|
19
|
+
browserWindow.emui = browserWindow.emui || {};
|
|
20
|
+
browserWindow.emui[appId] = browserWindow.emui[appId] || {};
|
|
21
|
+
const setFrameSize = () => {
|
|
22
|
+
const { document: document2 } = window;
|
|
23
|
+
const iframeBody = document2.body;
|
|
24
|
+
const iframeHTML = document2.documentElement;
|
|
25
|
+
const size = {
|
|
26
|
+
height: Math.max(
|
|
27
|
+
iframeBody.scrollHeight,
|
|
28
|
+
iframeBody.offsetHeight,
|
|
29
|
+
iframeHTML.offsetHeight
|
|
30
|
+
),
|
|
31
|
+
width: Math.max(
|
|
32
|
+
iframeBody.scrollWidth,
|
|
33
|
+
iframeBody.offsetWidth,
|
|
34
|
+
iframeHTML.offsetWidth
|
|
35
|
+
)
|
|
36
|
+
};
|
|
37
|
+
host.setAppWindowSize({
|
|
38
|
+
appId,
|
|
39
|
+
size
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
browserWindow.emui[appId].init = async (options) => {
|
|
43
|
+
host = options.host;
|
|
44
|
+
parentHistory = options.history;
|
|
45
|
+
logger = options.logger;
|
|
46
|
+
setFrameSize();
|
|
47
|
+
};
|
|
48
|
+
browserWindow.emui[appId].mount = async () => {
|
|
49
|
+
const appContainer = document.getElementById(appElementId);
|
|
50
|
+
if (appContainer) {
|
|
51
|
+
const mainElement = document.createElement("main");
|
|
52
|
+
appContainer.appendChild(mainElement);
|
|
53
|
+
const pageHeaderEle = document.createElement("h1");
|
|
54
|
+
pageHeaderEle.textContent = appName;
|
|
55
|
+
mainElement.appendChild(pageHeaderEle);
|
|
56
|
+
const contentEle = document.createElement("p");
|
|
57
|
+
contentEle.textContent = "Go to ";
|
|
58
|
+
mainElement.appendChild(contentEle);
|
|
59
|
+
const linkEle = document.createElement("a");
|
|
60
|
+
linkEle.href = "#";
|
|
61
|
+
linkEle.onclick = () => {
|
|
62
|
+
parentHistory?.push?.(pipelinePath);
|
|
63
|
+
};
|
|
64
|
+
linkEle.textContent = pipelineLinkText;
|
|
65
|
+
contentEle.appendChild(linkEle);
|
|
66
|
+
} else {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`App container element with id ${appElementId} not found`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
browserWindow.emui[appId].unmount = () => {
|
|
73
|
+
const appContainer = document.getElementById(appElementId);
|
|
74
|
+
if (appContainer) {
|
|
75
|
+
appContainer.removeChild(appContainer.getElementsByTagName("main")[0]);
|
|
76
|
+
}
|
|
77
|
+
return Promise.resolve();
|
|
78
|
+
};
|
|
79
|
+
})();
|