@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.
Files changed (37) hide show
  1. package/dist/cjs/frame.js +69 -27
  2. package/dist/cjs/loaders/manifest.js +2 -5
  3. package/dist/cjs/loaders/script.js +40 -36
  4. package/dist/cjs/utils/script-origin.js +45 -0
  5. package/dist/cjs/utils/webpack-public-path.js +20 -3
  6. package/dist/cjs/utils.js +17 -1
  7. package/dist/esm/frame.js +69 -27
  8. package/dist/esm/loaders/manifest.js +3 -6
  9. package/dist/esm/loaders/script.js +41 -40
  10. package/dist/esm/utils/script-origin.js +25 -0
  11. package/dist/esm/utils/webpack-public-path.js +20 -3
  12. package/dist/esm/utils.js +17 -1
  13. package/dist/public/e2e-host.html +1 -1
  14. package/dist/public/e2e-index.html +1 -1
  15. package/dist/public/frame.html +1 -1
  16. package/dist/public/index.html +1 -1
  17. package/dist/public/js/emuiAppBridge.d73405c86e4c6f166cfe.js +17 -0
  18. package/dist/public/js/emuiAppBridge.d73405c86e4c6f166cfe.js.br +0 -0
  19. package/dist/public/js/emuiAppBridge.d73405c86e4c6f166cfe.js.gz +0 -0
  20. package/dist/public/js/emuiAppBridge.d73405c86e4c6f166cfe.js.map +1 -0
  21. package/dist/types/lib/frame.d.ts +5 -0
  22. package/dist/types/lib/loaders/script.d.ts +5 -5
  23. package/dist/types/lib/tests/utils/script-origin.test.d.ts +1 -0
  24. package/dist/types/lib/typings/host.d.ts +4 -4
  25. package/dist/types/lib/utils/script-origin.d.ts +22 -0
  26. package/dist/types/lib/utils/webpack-public-path.d.ts +13 -2
  27. package/dist/types/lib/utils.d.ts +2 -0
  28. package/dist/types/tsconfig.tsbuildinfo +1 -1
  29. package/dist/umd/index.js +7 -7
  30. package/dist/umd/index.js.br +0 -0
  31. package/dist/umd/index.js.gz +0 -0
  32. package/dist/umd/index.js.map +1 -1
  33. package/package.json +3 -3
  34. package/dist/public/js/emuiAppBridge.13be9368ba5f8a719808.js +0 -17
  35. package/dist/public/js/emuiAppBridge.13be9368ba5f8a719808.js.br +0 -0
  36. package/dist/public/js/emuiAppBridge.13be9368ba5f8a719808.js.gz +0 -0
  37. 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
- const baseSrc = options.src ?? (import_frame.default.default ?? import_frame.default);
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
- const timeoutId = setTimeout(() => {
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
- clearTimeout(timeoutId);
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
- clearTimeout(timeoutId);
98
- const documentEle = frame.contentDocument;
99
- const ele = documentEle.getElementById(FRAME_APP_CONTAINER_ID_PREFIX);
100
- if (ele) {
101
- ele.id = `${ele.id}${id}`;
102
- }
103
- let baseTag = documentEle.getElementsByTagName("base")?.[0];
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 url = new URL(`${manifestPath}manifest.json`, hostUrl);
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.removeDoubleSlash)(
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 like inline HTML module tags:
114
- * synchronous insert for each batch, parallel fetch, execution in order.
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
- #loadOrderedModuleScripts = async (scripts, documentEle, isESMModule) => {
123
- if (scripts.length === 0) return;
124
- const appendScripts = (entries) => entries.map(
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
- const globalIndex = scripts.findIndex(
128
- ({ href }) => import_webpack_public_path.GLOBAL_SCRIPT_PATTERN.test(href)
129
- );
130
- if (globalIndex === -1) {
131
- await Promise.all(appendScripts(scripts));
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 throughGlobal = scripts.slice(0, globalIndex + 1);
135
- const afterGlobal = scripts.slice(globalIndex + 1);
136
- const throughGlobalPromises = appendScripts(throughGlobal);
137
- await throughGlobalPromises[throughGlobalPromises.length - 1];
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
- * - Preserves manifest order (CDN and local scripts stay in their original sequence)
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 in batches so the browser can fetch in parallel
161
- * - Relies on module script execution order (document order) within each batch
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
- (0, import_webpack_public_path.syncWebpackPublicPathFromAssetPath)(documentEle.defaultView);
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
- GLOBAL_SCRIPT_PATTERN: () => GLOBAL_SCRIPT_PATTERN,
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 GLOBAL_SCRIPT_PATTERN = /\/global(?:-prod)?\.js(?:\?|#|$)/i;
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
- const baseSrc = options.src ?? (frameHtml.default ?? frameHtml);
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
- const timeoutId = setTimeout(() => {
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
- clearTimeout(timeoutId);
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
- clearTimeout(timeoutId);
64
- const documentEle = frame.contentDocument;
65
- const ele = documentEle.getElementById(FRAME_APP_CONTAINER_ID_PREFIX);
66
- if (ele) {
67
- ele.id = `${ele.id}${id}`;
68
- }
69
- let baseTag = documentEle.getElementsByTagName("base")?.[0];
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 { removeDoubleSlash } from "../utils.js";
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 url = new URL(`${manifestPath}manifest.json`, hostUrl);
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 = removeDoubleSlash(
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
- GLOBAL_SCRIPT_PATTERN,
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 like inline HTML module tags:
93
- * synchronous insert for each batch, parallel fetch, execution in order.
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
- #loadOrderedModuleScripts = async (scripts, documentEle, isESMModule) => {
102
- if (scripts.length === 0) return;
103
- const appendScripts = (entries) => entries.map(
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
- const globalIndex = scripts.findIndex(
107
- ({ href }) => GLOBAL_SCRIPT_PATTERN.test(href)
108
- );
109
- if (globalIndex === -1) {
110
- await Promise.all(appendScripts(scripts));
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 throughGlobal = scripts.slice(0, globalIndex + 1);
114
- const afterGlobal = scripts.slice(globalIndex + 1);
115
- const throughGlobalPromises = appendScripts(throughGlobal);
116
- await throughGlobalPromises[throughGlobalPromises.length - 1];
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
- * - Preserves manifest order (CDN and local scripts stay in their original sequence)
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 in batches so the browser can fetch in parallel
140
- * - Relies on module script execution order (document order) within each batch
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
- syncWebpackPublicPathFromAssetPath(documentEle.defaultView);
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
  /**