@elliemae/pui-app-bridge 2.28.3 → 2.28.5
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/frame.js +69 -27
- package/dist/cjs/loaders/manifest.js +2 -5
- package/dist/cjs/loaders/script.js +40 -36
- package/dist/cjs/utils/script-origin.js +45 -0
- package/dist/cjs/utils/webpack-public-path.js +20 -3
- package/dist/cjs/utils.js +17 -1
- package/dist/esm/frame.js +69 -27
- package/dist/esm/loaders/manifest.js +3 -6
- package/dist/esm/loaders/script.js +41 -40
- package/dist/esm/utils/script-origin.js +25 -0
- package/dist/esm/utils/webpack-public-path.js +20 -3
- package/dist/esm/utils.js +17 -1
- 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.d73405c86e4c6f166cfe.js +17 -0
- package/dist/public/js/emuiAppBridge.d73405c86e4c6f166cfe.js.br +0 -0
- package/dist/public/js/emuiAppBridge.d73405c86e4c6f166cfe.js.gz +0 -0
- package/dist/public/js/emuiAppBridge.d73405c86e4c6f166cfe.js.map +1 -0
- package/dist/types/lib/frame.d.ts +5 -0
- package/dist/types/lib/loaders/script.d.ts +5 -5
- package/dist/types/lib/tests/utils/script-origin.test.d.ts +1 -0
- package/dist/types/lib/typings/host.d.ts +4 -4
- package/dist/types/lib/utils/script-origin.d.ts +22 -0
- package/dist/types/lib/utils/webpack-public-path.d.ts +13 -2
- package/dist/types/lib/utils.d.ts +2 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/umd/index.js +7 -7
- 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 +3 -3
- package/dist/public/js/emuiAppBridge.13be9368ba5f8a719808.js +0 -17
- package/dist/public/js/emuiAppBridge.13be9368ba5f8a719808.js.br +0 -0
- package/dist/public/js/emuiAppBridge.13be9368ba5f8a719808.js.gz +0 -0
- package/dist/public/js/emuiAppBridge.13be9368ba5f8a719808.js.map +0 -1
package/dist/cjs/frame.js
CHANGED
|
@@ -33,7 +33,36 @@ __export(frame_exports, {
|
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(frame_exports);
|
|
35
35
|
var import_frame = __toESM(require("./frame.html?resource"), 1);
|
|
36
|
+
var import_utils = require("./utils.js");
|
|
36
37
|
const FRAME_APP_CONTAINER_ID_PREFIX = "pui-app-container-";
|
|
38
|
+
const buildFrameSrc = (options) => {
|
|
39
|
+
const baseSrc = options.src ?? (import_frame.default.default ?? import_frame.default);
|
|
40
|
+
if (!options.queryParams) return baseSrc;
|
|
41
|
+
const sanitized = options.queryParams.replace(/[^a-zA-Z0-9&=_.~%+-]/g, "");
|
|
42
|
+
return `${baseSrc}${baseSrc.includes("?") ? "&" : "?"}${sanitized}`;
|
|
43
|
+
};
|
|
44
|
+
const appendManifestBaseTag = ({
|
|
45
|
+
documentEle,
|
|
46
|
+
hostUrl,
|
|
47
|
+
manifestPath,
|
|
48
|
+
guestId
|
|
49
|
+
}) => {
|
|
50
|
+
const containerEle = documentEle.getElementById(
|
|
51
|
+
FRAME_APP_CONTAINER_ID_PREFIX
|
|
52
|
+
);
|
|
53
|
+
if (containerEle) {
|
|
54
|
+
containerEle.id = `${containerEle.id}${guestId}`;
|
|
55
|
+
}
|
|
56
|
+
const manifestBaseUrl = (0, import_utils.resolveManifestBaseUrl)(hostUrl, manifestPath);
|
|
57
|
+
let baseTag = documentEle.getElementsByTagName("base")?.[0];
|
|
58
|
+
if (baseTag) {
|
|
59
|
+
baseTag.href = manifestBaseUrl;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
baseTag = documentEle.createElement("base");
|
|
63
|
+
baseTag.href = manifestBaseUrl;
|
|
64
|
+
documentEle.getElementsByTagName("head")[0].appendChild(baseTag);
|
|
65
|
+
};
|
|
37
66
|
const create = ({
|
|
38
67
|
id,
|
|
39
68
|
instanceId,
|
|
@@ -43,6 +72,11 @@ const create = ({
|
|
|
43
72
|
}) => (
|
|
44
73
|
// eslint-disable-next-line max-statements
|
|
45
74
|
new Promise((resolve, reject) => {
|
|
75
|
+
const { signal } = options;
|
|
76
|
+
if (signal?.aborted) {
|
|
77
|
+
reject(new DOMException("iframe creation aborted", "AbortError"));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
46
80
|
const iframeContainer = document.createElement("div");
|
|
47
81
|
iframeContainer.setAttribute(
|
|
48
82
|
"style",
|
|
@@ -61,21 +95,35 @@ const create = ({
|
|
|
61
95
|
"style",
|
|
62
96
|
options.style ?? "flex-grow: 1;border: none;margin: 0;padding: 0;display: block;min-width: 100%;height: 100%;"
|
|
63
97
|
);
|
|
64
|
-
|
|
65
|
-
let srcWithParams = baseSrc;
|
|
66
|
-
if (options.queryParams) {
|
|
67
|
-
const sanitized = options.queryParams.replace(
|
|
68
|
-
/[^a-zA-Z0-9&=_.~%+-]/g,
|
|
69
|
-
""
|
|
70
|
-
);
|
|
71
|
-
srcWithParams = `${baseSrc}${baseSrc.includes("?") ? "&" : "?"}${sanitized}`;
|
|
72
|
-
}
|
|
73
|
-
frame.setAttribute("src", srcWithParams);
|
|
98
|
+
frame.setAttribute("src", buildFrameSrc(options));
|
|
74
99
|
const FRAME_LOAD_TIMEOUT_MS = 1e4;
|
|
75
100
|
let settled = false;
|
|
76
|
-
|
|
101
|
+
let timeoutId;
|
|
102
|
+
let onAbort;
|
|
103
|
+
const cleanup = () => {
|
|
104
|
+
clearTimeout(timeoutId);
|
|
105
|
+
signal?.removeEventListener("abort", onAbort);
|
|
106
|
+
};
|
|
107
|
+
const rejectIfAborted = () => {
|
|
108
|
+
if (!signal?.aborted) return false;
|
|
109
|
+
if (!settled) {
|
|
110
|
+
settled = true;
|
|
111
|
+
cleanup();
|
|
112
|
+
iframeContainer.remove();
|
|
113
|
+
reject(new DOMException("iframe creation aborted", "AbortError"));
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
};
|
|
117
|
+
onAbort = () => {
|
|
118
|
+
rejectIfAborted();
|
|
119
|
+
};
|
|
120
|
+
if (rejectIfAborted()) return;
|
|
121
|
+
signal?.addEventListener("abort", onAbort);
|
|
122
|
+
timeoutId = setTimeout(() => {
|
|
77
123
|
if (!settled) {
|
|
78
124
|
settled = true;
|
|
125
|
+
cleanup();
|
|
126
|
+
iframeContainer.remove();
|
|
79
127
|
reject(
|
|
80
128
|
new Error(
|
|
81
129
|
`iframe for ${id} failed to load within ${FRAME_LOAD_TIMEOUT_MS}ms`
|
|
@@ -86,28 +134,22 @@ const create = ({
|
|
|
86
134
|
frame.addEventListener("error", () => {
|
|
87
135
|
if (!settled) {
|
|
88
136
|
settled = true;
|
|
89
|
-
|
|
137
|
+
cleanup();
|
|
138
|
+
iframeContainer.remove();
|
|
90
139
|
reject(new Error(`iframe for ${id} failed to load`));
|
|
91
140
|
}
|
|
92
141
|
});
|
|
93
142
|
frame.addEventListener("load", () => {
|
|
94
|
-
if (settled) return;
|
|
143
|
+
if (settled || rejectIfAborted()) return;
|
|
95
144
|
if (!frame.contentDocument) return;
|
|
96
145
|
settled = true;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (baseTag) {
|
|
105
|
-
baseTag.href = new URL(manifestPath, hostUrl).href;
|
|
106
|
-
} else {
|
|
107
|
-
baseTag = documentEle.createElement("base");
|
|
108
|
-
baseTag.href = new URL(manifestPath, hostUrl).href;
|
|
109
|
-
documentEle.getElementsByTagName("head")[0].appendChild(baseTag);
|
|
110
|
-
}
|
|
146
|
+
cleanup();
|
|
147
|
+
appendManifestBaseTag({
|
|
148
|
+
documentEle: frame.contentDocument,
|
|
149
|
+
hostUrl,
|
|
150
|
+
manifestPath,
|
|
151
|
+
guestId: id
|
|
152
|
+
});
|
|
111
153
|
resolve(frame);
|
|
112
154
|
});
|
|
113
155
|
iframeContainer.appendChild(frame);
|
|
@@ -38,8 +38,7 @@ const get = async ({
|
|
|
38
38
|
hostUrl,
|
|
39
39
|
manifestPath
|
|
40
40
|
}) => {
|
|
41
|
-
const
|
|
42
|
-
const manifestUrl = (0, import_utils.removeDoubleSlash)(url.href);
|
|
41
|
+
const manifestUrl = (0, import_utils.resolveManifestUrl)(hostUrl, manifestPath);
|
|
43
42
|
const cached = manifestCache.get(manifestUrl);
|
|
44
43
|
if (cached) return cached;
|
|
45
44
|
const fetchManifest = async (fetchUrl) => {
|
|
@@ -60,9 +59,7 @@ const get = async ({
|
|
|
60
59
|
} catch (error) {
|
|
61
60
|
const unVersionedManifestPath = getUnVersionedManifestPath(manifestPath);
|
|
62
61
|
if (manifestPath !== unVersionedManifestPath) {
|
|
63
|
-
const fallbackUrl = (0, import_utils.
|
|
64
|
-
new URL(`${unVersionedManifestPath}manifest.json`, hostUrl).href
|
|
65
|
-
);
|
|
62
|
+
const fallbackUrl = (0, import_utils.resolveManifestUrl)(hostUrl, unVersionedManifestPath);
|
|
66
63
|
return await fetchManifest(fallbackUrl);
|
|
67
64
|
}
|
|
68
65
|
throw error;
|
|
@@ -23,6 +23,7 @@ __export(script_exports, {
|
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(script_exports);
|
|
25
25
|
var import_utils = require("../utils.js");
|
|
26
|
+
var import_script_origin = require("../utils/script-origin.js");
|
|
26
27
|
var import_webpack_public_path = require("../utils/webpack-public-path.js");
|
|
27
28
|
const APP_SCRIPT_ID_PREFIX = "ice-script-";
|
|
28
29
|
class ScriptLoader {
|
|
@@ -110,39 +111,41 @@ class ScriptLoader {
|
|
|
110
111
|
documentEle.head.appendChild(ele);
|
|
111
112
|
});
|
|
112
113
|
/**
|
|
113
|
-
* Appends script elements in document order
|
|
114
|
-
*
|
|
115
|
-
* Scripts through global.js are one batch; public path is synced after
|
|
116
|
-
* global.js loads, then the remaining tags are inserted as a second batch.
|
|
117
|
-
* @param scripts
|
|
114
|
+
* Appends script elements in document order and waits for each to load.
|
|
115
|
+
* @param entries
|
|
118
116
|
* @param documentEle
|
|
119
117
|
* @param isESMModule
|
|
120
|
-
* @private
|
|
121
118
|
*/
|
|
122
|
-
#
|
|
123
|
-
if (
|
|
124
|
-
const
|
|
119
|
+
#loadAndWaitForScripts = async (entries, documentEle, isESMModule) => {
|
|
120
|
+
if (entries.length === 0) return;
|
|
121
|
+
const promises = entries.map(
|
|
125
122
|
({ id, href }) => this.#loadScript({ id, href, documentEle, isESMModule })
|
|
126
123
|
);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
await Promise.all(promises);
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Stages external scripts before local guest-host scripts.
|
|
128
|
+
* @param root0
|
|
129
|
+
* @param root0.scripts
|
|
130
|
+
* @param root0.documentEle
|
|
131
|
+
* @param root0.isESMModule
|
|
132
|
+
* @param root0.hostUrl
|
|
133
|
+
*/
|
|
134
|
+
#loadOrderedModuleScripts = async ({
|
|
135
|
+
scripts,
|
|
136
|
+
documentEle,
|
|
137
|
+
isESMModule,
|
|
138
|
+
hostUrl
|
|
139
|
+
}) => {
|
|
140
|
+
if (scripts.length === 0) return;
|
|
141
|
+
if (!hostUrl) {
|
|
142
|
+
await this.#loadAndWaitForScripts(scripts, documentEle, isESMModule);
|
|
132
143
|
return;
|
|
133
144
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
await
|
|
138
|
-
(0, import_webpack_public_path.syncWebpackPublicPathFromAssetPath)(documentEle.defaultView);
|
|
139
|
-
const pendingPromises = [
|
|
140
|
-
...throughGlobalPromises.slice(0, -1),
|
|
141
|
-
...appendScripts(afterGlobal)
|
|
142
|
-
];
|
|
143
|
-
if (pendingPromises.length > 0) {
|
|
144
|
-
await Promise.all(pendingPromises);
|
|
145
|
-
}
|
|
145
|
+
const { external, local } = (0, import_script_origin.partitionScriptsByOrigin)(scripts, hostUrl);
|
|
146
|
+
await this.#loadAndWaitForScripts(external, documentEle, isESMModule);
|
|
147
|
+
(0, import_webpack_public_path.syncWebpackPublicPath)(documentEle.defaultView, hostUrl);
|
|
148
|
+
await this.#loadAndWaitForScripts(local, documentEle, isESMModule);
|
|
146
149
|
};
|
|
147
150
|
/**
|
|
148
151
|
* Loads multiple script assets in manifest order, like HTML module script tags.
|
|
@@ -155,18 +158,18 @@ class ScriptLoader {
|
|
|
155
158
|
* @returns A promise that resolves with an array of script element IDs when all scripts are loaded
|
|
156
159
|
* @remarks
|
|
157
160
|
* This method:
|
|
158
|
-
* -
|
|
161
|
+
* - Stages external (CDN) scripts before local guest-host scripts
|
|
162
|
+
* - Preserves manifest order within each origin group
|
|
159
163
|
* - Creates modulepreload links for all assets for performance optimization
|
|
160
|
-
* - Inserts script tags synchronously
|
|
161
|
-
* -
|
|
162
|
-
* - Waits for global.js before inserting dependent bundles, then syncs webpack public path
|
|
164
|
+
* - Inserts script tags synchronously per stage so the browser can fetch in parallel
|
|
165
|
+
* - Syncs webpack public path from hostUrl (or guest _ASSET_PATH) before local scripts run
|
|
163
166
|
* @example
|
|
164
167
|
* ```typescript
|
|
165
168
|
* const scriptIds = await scriptLoader.load(
|
|
166
169
|
* ['https://cdn.example.com/lib.js', 'global.js', 'runtime~app.js'],
|
|
167
170
|
* {
|
|
168
171
|
* name: 'myApp',
|
|
169
|
-
* hostUrl: 'https://example.com',
|
|
172
|
+
* hostUrl: 'https://example.com/my-app/',
|
|
170
173
|
* documentEle: document,
|
|
171
174
|
* isESMModule: true
|
|
172
175
|
* }
|
|
@@ -182,12 +185,13 @@ class ScriptLoader {
|
|
|
182
185
|
this.#preLoadScript({ href, documentEle, isESMModule });
|
|
183
186
|
return { id, href };
|
|
184
187
|
});
|
|
185
|
-
await this.#loadOrderedModuleScripts(
|
|
186
|
-
orderedScripts,
|
|
188
|
+
await this.#loadOrderedModuleScripts({
|
|
189
|
+
scripts: orderedScripts,
|
|
187
190
|
documentEle,
|
|
188
|
-
isESMModule
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
isESMModule,
|
|
192
|
+
hostUrl: options.hostUrl
|
|
193
|
+
});
|
|
194
|
+
(0, import_webpack_public_path.syncWebpackPublicPath)(documentEle.defaultView, options.hostUrl);
|
|
191
195
|
return orderedScripts.map((asset) => asset.id);
|
|
192
196
|
};
|
|
193
197
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var script_origin_exports = {};
|
|
20
|
+
__export(script_origin_exports, {
|
|
21
|
+
isExternalScriptHref: () => isExternalScriptHref,
|
|
22
|
+
partitionScriptsByOrigin: () => partitionScriptsByOrigin
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(script_origin_exports);
|
|
25
|
+
const HTTP_PATTERN = /^https?:\/\//i;
|
|
26
|
+
const isExternalScriptHref = (href, hostUrl) => {
|
|
27
|
+
if (!HTTP_PATTERN.test(href)) return false;
|
|
28
|
+
try {
|
|
29
|
+
return !href.startsWith(new URL(hostUrl).href);
|
|
30
|
+
} catch {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const partitionScriptsByOrigin = (scripts, hostUrl) => {
|
|
35
|
+
const external = [];
|
|
36
|
+
const local = [];
|
|
37
|
+
scripts.forEach((script) => {
|
|
38
|
+
if (isExternalScriptHref(script.href, hostUrl)) {
|
|
39
|
+
external.push(script);
|
|
40
|
+
} else {
|
|
41
|
+
local.push(script);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return { external, local };
|
|
45
|
+
};
|
|
@@ -18,8 +18,9 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var webpack_public_path_exports = {};
|
|
20
20
|
__export(webpack_public_path_exports, {
|
|
21
|
-
|
|
22
|
-
syncWebpackPublicPathFromAssetPath: () => syncWebpackPublicPathFromAssetPath
|
|
21
|
+
syncWebpackPublicPath: () => syncWebpackPublicPath,
|
|
22
|
+
syncWebpackPublicPathFromAssetPath: () => syncWebpackPublicPathFromAssetPath,
|
|
23
|
+
syncWebpackPublicPathFromHost: () => syncWebpackPublicPathFromHost
|
|
23
24
|
});
|
|
24
25
|
module.exports = __toCommonJS(webpack_public_path_exports);
|
|
25
26
|
const syncWebpackPublicPathFromAssetPath = (targetWindow) => {
|
|
@@ -29,4 +30,20 @@ const syncWebpackPublicPathFromAssetPath = (targetWindow) => {
|
|
|
29
30
|
if (!assetPath) return;
|
|
30
31
|
win.__webpack_public_path__ = new URL("../", assetPath).href;
|
|
31
32
|
};
|
|
32
|
-
const
|
|
33
|
+
const syncWebpackPublicPathFromHost = (targetWindow, hostUrl) => {
|
|
34
|
+
if (!targetWindow || !hostUrl) return;
|
|
35
|
+
try {
|
|
36
|
+
const win = targetWindow;
|
|
37
|
+
win.__webpack_public_path__ = new URL("./", hostUrl).href;
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const syncWebpackPublicPath = (targetWindow, hostUrl) => {
|
|
42
|
+
if (!targetWindow) return;
|
|
43
|
+
const win = targetWindow;
|
|
44
|
+
if (win.emui?._ASSET_PATH) {
|
|
45
|
+
syncWebpackPublicPathFromAssetPath(targetWindow);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
syncWebpackPublicPathFromHost(targetWindow, hostUrl);
|
|
49
|
+
};
|
package/dist/cjs/utils.js
CHANGED
|
@@ -23,7 +23,9 @@ __export(utils_exports, {
|
|
|
23
23
|
escapeRegExp: () => escapeRegExp,
|
|
24
24
|
getAbsoluteUrl: () => getAbsoluteUrl,
|
|
25
25
|
isJSDOM: () => isJSDOM,
|
|
26
|
-
removeDoubleSlash: () => removeDoubleSlash
|
|
26
|
+
removeDoubleSlash: () => removeDoubleSlash,
|
|
27
|
+
resolveManifestBaseUrl: () => resolveManifestBaseUrl,
|
|
28
|
+
resolveManifestUrl: () => resolveManifestUrl
|
|
27
29
|
});
|
|
28
30
|
module.exports = __toCommonJS(utils_exports);
|
|
29
31
|
const removeDoubleSlash = (url) => url.replace(/([^:]\/)\/+/g, "$1");
|
|
@@ -34,5 +36,19 @@ const getAbsoluteUrl = (url) => {
|
|
|
34
36
|
};
|
|
35
37
|
const appendTrailingSlash = (url) => url?.replace?.(/\/?$/, "/");
|
|
36
38
|
const appendPath = (base, path) => `${base.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
39
|
+
const isAbsoluteManifestPath = (manifestPath) => /^(\/|https?:\/\/)/.test(manifestPath);
|
|
40
|
+
const resolveManifestBaseUrl = (hostUrl, manifestPath) => {
|
|
41
|
+
const normalizedPath = appendTrailingSlash(manifestPath);
|
|
42
|
+
if (isAbsoluteManifestPath(normalizedPath)) {
|
|
43
|
+
const { origin } = new URL(hostUrl, window.location.href);
|
|
44
|
+
return removeDoubleSlash(new URL(normalizedPath, origin).href);
|
|
45
|
+
}
|
|
46
|
+
return removeDoubleSlash(
|
|
47
|
+
new URL(normalizedPath, appendTrailingSlash(hostUrl)).href
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
const resolveManifestUrl = (hostUrl, manifestPath) => removeDoubleSlash(
|
|
51
|
+
new URL("manifest.json", resolveManifestBaseUrl(hostUrl, manifestPath)).href
|
|
52
|
+
);
|
|
37
53
|
const isJSDOM = () => window.navigator.userAgent.includes("jsdom");
|
|
38
54
|
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
package/dist/esm/frame.js
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import frameHtml from "./frame.html?resource";
|
|
2
|
+
import { resolveManifestBaseUrl } from "./utils.js";
|
|
2
3
|
const FRAME_APP_CONTAINER_ID_PREFIX = "pui-app-container-";
|
|
4
|
+
const buildFrameSrc = (options) => {
|
|
5
|
+
const baseSrc = options.src ?? (frameHtml.default ?? frameHtml);
|
|
6
|
+
if (!options.queryParams) return baseSrc;
|
|
7
|
+
const sanitized = options.queryParams.replace(/[^a-zA-Z0-9&=_.~%+-]/g, "");
|
|
8
|
+
return `${baseSrc}${baseSrc.includes("?") ? "&" : "?"}${sanitized}`;
|
|
9
|
+
};
|
|
10
|
+
const appendManifestBaseTag = ({
|
|
11
|
+
documentEle,
|
|
12
|
+
hostUrl,
|
|
13
|
+
manifestPath,
|
|
14
|
+
guestId
|
|
15
|
+
}) => {
|
|
16
|
+
const containerEle = documentEle.getElementById(
|
|
17
|
+
FRAME_APP_CONTAINER_ID_PREFIX
|
|
18
|
+
);
|
|
19
|
+
if (containerEle) {
|
|
20
|
+
containerEle.id = `${containerEle.id}${guestId}`;
|
|
21
|
+
}
|
|
22
|
+
const manifestBaseUrl = resolveManifestBaseUrl(hostUrl, manifestPath);
|
|
23
|
+
let baseTag = documentEle.getElementsByTagName("base")?.[0];
|
|
24
|
+
if (baseTag) {
|
|
25
|
+
baseTag.href = manifestBaseUrl;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
baseTag = documentEle.createElement("base");
|
|
29
|
+
baseTag.href = manifestBaseUrl;
|
|
30
|
+
documentEle.getElementsByTagName("head")[0].appendChild(baseTag);
|
|
31
|
+
};
|
|
3
32
|
const create = ({
|
|
4
33
|
id,
|
|
5
34
|
instanceId,
|
|
@@ -9,6 +38,11 @@ const create = ({
|
|
|
9
38
|
}) => (
|
|
10
39
|
// eslint-disable-next-line max-statements
|
|
11
40
|
new Promise((resolve, reject) => {
|
|
41
|
+
const { signal } = options;
|
|
42
|
+
if (signal?.aborted) {
|
|
43
|
+
reject(new DOMException("iframe creation aborted", "AbortError"));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
12
46
|
const iframeContainer = document.createElement("div");
|
|
13
47
|
iframeContainer.setAttribute(
|
|
14
48
|
"style",
|
|
@@ -27,21 +61,35 @@ const create = ({
|
|
|
27
61
|
"style",
|
|
28
62
|
options.style ?? "flex-grow: 1;border: none;margin: 0;padding: 0;display: block;min-width: 100%;height: 100%;"
|
|
29
63
|
);
|
|
30
|
-
|
|
31
|
-
let srcWithParams = baseSrc;
|
|
32
|
-
if (options.queryParams) {
|
|
33
|
-
const sanitized = options.queryParams.replace(
|
|
34
|
-
/[^a-zA-Z0-9&=_.~%+-]/g,
|
|
35
|
-
""
|
|
36
|
-
);
|
|
37
|
-
srcWithParams = `${baseSrc}${baseSrc.includes("?") ? "&" : "?"}${sanitized}`;
|
|
38
|
-
}
|
|
39
|
-
frame.setAttribute("src", srcWithParams);
|
|
64
|
+
frame.setAttribute("src", buildFrameSrc(options));
|
|
40
65
|
const FRAME_LOAD_TIMEOUT_MS = 1e4;
|
|
41
66
|
let settled = false;
|
|
42
|
-
|
|
67
|
+
let timeoutId;
|
|
68
|
+
let onAbort;
|
|
69
|
+
const cleanup = () => {
|
|
70
|
+
clearTimeout(timeoutId);
|
|
71
|
+
signal?.removeEventListener("abort", onAbort);
|
|
72
|
+
};
|
|
73
|
+
const rejectIfAborted = () => {
|
|
74
|
+
if (!signal?.aborted) return false;
|
|
75
|
+
if (!settled) {
|
|
76
|
+
settled = true;
|
|
77
|
+
cleanup();
|
|
78
|
+
iframeContainer.remove();
|
|
79
|
+
reject(new DOMException("iframe creation aborted", "AbortError"));
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
};
|
|
83
|
+
onAbort = () => {
|
|
84
|
+
rejectIfAborted();
|
|
85
|
+
};
|
|
86
|
+
if (rejectIfAborted()) return;
|
|
87
|
+
signal?.addEventListener("abort", onAbort);
|
|
88
|
+
timeoutId = setTimeout(() => {
|
|
43
89
|
if (!settled) {
|
|
44
90
|
settled = true;
|
|
91
|
+
cleanup();
|
|
92
|
+
iframeContainer.remove();
|
|
45
93
|
reject(
|
|
46
94
|
new Error(
|
|
47
95
|
`iframe for ${id} failed to load within ${FRAME_LOAD_TIMEOUT_MS}ms`
|
|
@@ -52,28 +100,22 @@ const create = ({
|
|
|
52
100
|
frame.addEventListener("error", () => {
|
|
53
101
|
if (!settled) {
|
|
54
102
|
settled = true;
|
|
55
|
-
|
|
103
|
+
cleanup();
|
|
104
|
+
iframeContainer.remove();
|
|
56
105
|
reject(new Error(`iframe for ${id} failed to load`));
|
|
57
106
|
}
|
|
58
107
|
});
|
|
59
108
|
frame.addEventListener("load", () => {
|
|
60
|
-
if (settled) return;
|
|
109
|
+
if (settled || rejectIfAborted()) return;
|
|
61
110
|
if (!frame.contentDocument) return;
|
|
62
111
|
settled = true;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (baseTag) {
|
|
71
|
-
baseTag.href = new URL(manifestPath, hostUrl).href;
|
|
72
|
-
} else {
|
|
73
|
-
baseTag = documentEle.createElement("base");
|
|
74
|
-
baseTag.href = new URL(manifestPath, hostUrl).href;
|
|
75
|
-
documentEle.getElementsByTagName("head")[0].appendChild(baseTag);
|
|
76
|
-
}
|
|
112
|
+
cleanup();
|
|
113
|
+
appendManifestBaseTag({
|
|
114
|
+
documentEle: frame.contentDocument,
|
|
115
|
+
hostUrl,
|
|
116
|
+
manifestPath,
|
|
117
|
+
guestId: id
|
|
118
|
+
});
|
|
77
119
|
resolve(frame);
|
|
78
120
|
});
|
|
79
121
|
iframeContainer.appendChild(frame);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveManifestUrl } from "../utils.js";
|
|
2
2
|
const getUnVersionedManifestPath = (path) => path.replace(/\/\d+\.\d+/, "/latest");
|
|
3
3
|
const isValidHttpUrl = (fileName) => {
|
|
4
4
|
let url;
|
|
@@ -15,8 +15,7 @@ const get = async ({
|
|
|
15
15
|
hostUrl,
|
|
16
16
|
manifestPath
|
|
17
17
|
}) => {
|
|
18
|
-
const
|
|
19
|
-
const manifestUrl = removeDoubleSlash(url.href);
|
|
18
|
+
const manifestUrl = resolveManifestUrl(hostUrl, manifestPath);
|
|
20
19
|
const cached = manifestCache.get(manifestUrl);
|
|
21
20
|
if (cached) return cached;
|
|
22
21
|
const fetchManifest = async (fetchUrl) => {
|
|
@@ -37,9 +36,7 @@ const get = async ({
|
|
|
37
36
|
} catch (error) {
|
|
38
37
|
const unVersionedManifestPath = getUnVersionedManifestPath(manifestPath);
|
|
39
38
|
if (manifestPath !== unVersionedManifestPath) {
|
|
40
|
-
const fallbackUrl =
|
|
41
|
-
new URL(`${unVersionedManifestPath}manifest.json`, hostUrl).href
|
|
42
|
-
);
|
|
39
|
+
const fallbackUrl = resolveManifestUrl(hostUrl, unVersionedManifestPath);
|
|
43
40
|
return await fetchManifest(fallbackUrl);
|
|
44
41
|
}
|
|
45
42
|
throw error;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { removeDoubleSlash, isJSDOM, escapeRegExp } from "../utils.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
syncWebpackPublicPathFromAssetPath
|
|
5
|
-
} from "../utils/webpack-public-path.js";
|
|
2
|
+
import { partitionScriptsByOrigin } from "../utils/script-origin.js";
|
|
3
|
+
import { syncWebpackPublicPath } from "../utils/webpack-public-path.js";
|
|
6
4
|
const APP_SCRIPT_ID_PREFIX = "ice-script-";
|
|
7
5
|
class ScriptLoader {
|
|
8
6
|
#logger;
|
|
@@ -89,39 +87,41 @@ class ScriptLoader {
|
|
|
89
87
|
documentEle.head.appendChild(ele);
|
|
90
88
|
});
|
|
91
89
|
/**
|
|
92
|
-
* Appends script elements in document order
|
|
93
|
-
*
|
|
94
|
-
* Scripts through global.js are one batch; public path is synced after
|
|
95
|
-
* global.js loads, then the remaining tags are inserted as a second batch.
|
|
96
|
-
* @param scripts
|
|
90
|
+
* Appends script elements in document order and waits for each to load.
|
|
91
|
+
* @param entries
|
|
97
92
|
* @param documentEle
|
|
98
93
|
* @param isESMModule
|
|
99
|
-
* @private
|
|
100
94
|
*/
|
|
101
|
-
#
|
|
102
|
-
if (
|
|
103
|
-
const
|
|
95
|
+
#loadAndWaitForScripts = async (entries, documentEle, isESMModule) => {
|
|
96
|
+
if (entries.length === 0) return;
|
|
97
|
+
const promises = entries.map(
|
|
104
98
|
({ id, href }) => this.#loadScript({ id, href, documentEle, isESMModule })
|
|
105
99
|
);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
await Promise.all(promises);
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Stages external scripts before local guest-host scripts.
|
|
104
|
+
* @param root0
|
|
105
|
+
* @param root0.scripts
|
|
106
|
+
* @param root0.documentEle
|
|
107
|
+
* @param root0.isESMModule
|
|
108
|
+
* @param root0.hostUrl
|
|
109
|
+
*/
|
|
110
|
+
#loadOrderedModuleScripts = async ({
|
|
111
|
+
scripts,
|
|
112
|
+
documentEle,
|
|
113
|
+
isESMModule,
|
|
114
|
+
hostUrl
|
|
115
|
+
}) => {
|
|
116
|
+
if (scripts.length === 0) return;
|
|
117
|
+
if (!hostUrl) {
|
|
118
|
+
await this.#loadAndWaitForScripts(scripts, documentEle, isESMModule);
|
|
111
119
|
return;
|
|
112
120
|
}
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
await
|
|
117
|
-
syncWebpackPublicPathFromAssetPath(documentEle.defaultView);
|
|
118
|
-
const pendingPromises = [
|
|
119
|
-
...throughGlobalPromises.slice(0, -1),
|
|
120
|
-
...appendScripts(afterGlobal)
|
|
121
|
-
];
|
|
122
|
-
if (pendingPromises.length > 0) {
|
|
123
|
-
await Promise.all(pendingPromises);
|
|
124
|
-
}
|
|
121
|
+
const { external, local } = partitionScriptsByOrigin(scripts, hostUrl);
|
|
122
|
+
await this.#loadAndWaitForScripts(external, documentEle, isESMModule);
|
|
123
|
+
syncWebpackPublicPath(documentEle.defaultView, hostUrl);
|
|
124
|
+
await this.#loadAndWaitForScripts(local, documentEle, isESMModule);
|
|
125
125
|
};
|
|
126
126
|
/**
|
|
127
127
|
* Loads multiple script assets in manifest order, like HTML module script tags.
|
|
@@ -134,18 +134,18 @@ class ScriptLoader {
|
|
|
134
134
|
* @returns A promise that resolves with an array of script element IDs when all scripts are loaded
|
|
135
135
|
* @remarks
|
|
136
136
|
* This method:
|
|
137
|
-
* -
|
|
137
|
+
* - Stages external (CDN) scripts before local guest-host scripts
|
|
138
|
+
* - Preserves manifest order within each origin group
|
|
138
139
|
* - Creates modulepreload links for all assets for performance optimization
|
|
139
|
-
* - Inserts script tags synchronously
|
|
140
|
-
* -
|
|
141
|
-
* - Waits for global.js before inserting dependent bundles, then syncs webpack public path
|
|
140
|
+
* - Inserts script tags synchronously per stage so the browser can fetch in parallel
|
|
141
|
+
* - Syncs webpack public path from hostUrl (or guest _ASSET_PATH) before local scripts run
|
|
142
142
|
* @example
|
|
143
143
|
* ```typescript
|
|
144
144
|
* const scriptIds = await scriptLoader.load(
|
|
145
145
|
* ['https://cdn.example.com/lib.js', 'global.js', 'runtime~app.js'],
|
|
146
146
|
* {
|
|
147
147
|
* name: 'myApp',
|
|
148
|
-
* hostUrl: 'https://example.com',
|
|
148
|
+
* hostUrl: 'https://example.com/my-app/',
|
|
149
149
|
* documentEle: document,
|
|
150
150
|
* isESMModule: true
|
|
151
151
|
* }
|
|
@@ -161,12 +161,13 @@ class ScriptLoader {
|
|
|
161
161
|
this.#preLoadScript({ href, documentEle, isESMModule });
|
|
162
162
|
return { id, href };
|
|
163
163
|
});
|
|
164
|
-
await this.#loadOrderedModuleScripts(
|
|
165
|
-
orderedScripts,
|
|
164
|
+
await this.#loadOrderedModuleScripts({
|
|
165
|
+
scripts: orderedScripts,
|
|
166
166
|
documentEle,
|
|
167
|
-
isESMModule
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
isESMModule,
|
|
168
|
+
hostUrl: options.hostUrl
|
|
169
|
+
});
|
|
170
|
+
syncWebpackPublicPath(documentEle.defaultView, options.hostUrl);
|
|
170
171
|
return orderedScripts.map((asset) => asset.id);
|
|
171
172
|
};
|
|
172
173
|
/**
|