@openmrs/esm-dynamic-loading 10.0.1-pre.5299 → 10.0.1-pre.5316
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/.turbo/turbo-build.log
CHANGED
|
@@ -34,7 +34,8 @@ export declare function importDynamic<T = any>(jsPackage: string, share?: string
|
|
|
34
34
|
* @internal
|
|
35
35
|
*
|
|
36
36
|
* This internal method is used to ensure that the script belonging
|
|
37
|
-
* to the given package is loaded and added to the head.
|
|
37
|
+
* to the given package is loaded and added to the head. Concurrent and
|
|
38
|
+
* repeat calls for the same package share a single request for it.
|
|
38
39
|
*
|
|
39
40
|
* @param jsPackage The package to load
|
|
40
41
|
* @param importMap The import map to use for loading this package.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-loading.d.ts","sourceRoot":"","sources":["../src/dynamic-loading.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAI1E;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,UAEnC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,aAAa,CAAC,CAAC,GAAG,GAAG,EACzC,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,MAAkB,EACzB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GACA,OAAO,CAAC,CAAC,CAAC,CAyCZ;AAuBD
|
|
1
|
+
{"version":3,"file":"dynamic-loading.d.ts","sourceRoot":"","sources":["../src/dynamic-loading.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAI1E;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,UAEnC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,aAAa,CAAC,CAAC,GAAG,GAAG,EACzC,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,MAAkB,EACzB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GACA,OAAO,CAAC,CAAC,CAAC,CAyCZ;AAuBD;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,SAAS,iBAkD3E;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,uBAExC"}
|
package/dist/dynamic-loading.js
CHANGED
|
@@ -84,7 +84,8 @@ import { getCurrentPageMap, getImportMapOverrideMap, resetImportMapOverrides } f
|
|
|
84
84
|
* @internal
|
|
85
85
|
*
|
|
86
86
|
* This internal method is used to ensure that the script belonging
|
|
87
|
-
* to the given package is loaded and added to the head.
|
|
87
|
+
* to the given package is loaded and added to the head. Concurrent and
|
|
88
|
+
* repeat calls for the same package share a single request for it.
|
|
88
89
|
*
|
|
89
90
|
* @param jsPackage The package to load
|
|
90
91
|
* @param importMap The import map to use for loading this package.
|
|
@@ -110,9 +111,7 @@ import { getCurrentPageMap, getImportMapOverrideMap, resetImportMapOverrides } f
|
|
|
110
111
|
}
|
|
111
112
|
const isOverridden = jsPackage in getImportMapOverrideMap().imports;
|
|
112
113
|
try {
|
|
113
|
-
return await
|
|
114
|
-
loadScript(url, resolve, reject);
|
|
115
|
-
});
|
|
114
|
+
return await loadScript(url);
|
|
116
115
|
} catch (err) {
|
|
117
116
|
if (isOverridden) {
|
|
118
117
|
dispatchToastShown({
|
|
@@ -146,76 +145,65 @@ function isFederatedModule(a) {
|
|
|
146
145
|
return typeof a === 'object' && a !== null && 'init' in a && typeof a['init'] === 'function' && 'get' in a && typeof a['get'] === 'function';
|
|
147
146
|
}
|
|
148
147
|
// internals to track script loading
|
|
149
|
-
//
|
|
150
|
-
// we use a global to track this
|
|
151
|
-
|
|
148
|
+
// if we're already loading a script, we should wait until the script is loaded
|
|
149
|
+
// we use a global to track this; we use `Symbol.for()` in case this gets loaded
|
|
150
|
+
// twice
|
|
151
|
+
const OPENMRS_SCRIPT_LOADING = Symbol.for('__openmrs_script_loading');
|
|
152
152
|
/**
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
* The scripts this loader has requested, keyed by URL. A pending entry is in flight and a settled
|
|
154
|
+
* one has loaded; failed requests are removed, so any entry can be awaited rather than re-requested.
|
|
155
|
+
*/ function getScriptRegistry() {
|
|
156
|
+
var _window, _OPENMRS_SCRIPT_LOADING;
|
|
157
|
+
return (_window = window)[_OPENMRS_SCRIPT_LOADING = OPENMRS_SCRIPT_LOADING] ?? (_window[_OPENMRS_SCRIPT_LOADING] = new Map());
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Appends a `<script>` to the DOM with the given URL, unless it has been requested already, and
|
|
161
|
+
* resolves once it has loaded. Callers arriving at any point in a script's lifecycle share one
|
|
162
|
+
* request for it.
|
|
163
|
+
*
|
|
164
|
+
* A script which fails loading is removed from the DOM and can be retried. A script which throws
|
|
165
|
+
* when loading, however, will remain in the DOM as the script loaded.
|
|
166
|
+
*/ function loadScript(url) {
|
|
167
|
+
const registry = getScriptRegistry();
|
|
168
|
+
const requested = registry.get(url);
|
|
169
|
+
if (requested) {
|
|
170
|
+
return requested;
|
|
159
171
|
}
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
if (document.head.querySelector(`script[src="${url}"]`)) {
|
|
173
|
+
// something other than this loader put the script in the document, so assume it's usable
|
|
174
|
+
console.warn(`Script at ${url} already loaded. Not loading it again.`);
|
|
175
|
+
return Promise.resolve();
|
|
176
|
+
}
|
|
177
|
+
const element = document.createElement('script');
|
|
178
|
+
element.src = url;
|
|
179
|
+
element.type = 'text/javascript';
|
|
180
|
+
element.async = true;
|
|
181
|
+
const request = new Promise((resolve, reject)=>{
|
|
182
|
+
// displays an error if a script takes more than 5 seconds to load
|
|
167
183
|
const loadTimeout = setTimeout(()=>{
|
|
168
184
|
console.error(`The script at ${url} did not load within 5 seconds. This may indicate an issue with the imports in the script's entry-point file or with the script's bundler configuration.`);
|
|
169
185
|
}, 5000); // 5 seconds; this is arbitrary
|
|
170
|
-
|
|
171
|
-
finishScriptLoading = ()=>{
|
|
186
|
+
function finishScriptLoading() {
|
|
172
187
|
clearTimeout(loadTimeout);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (errFn) {
|
|
178
|
-
element.removeEventListener('error', errFn);
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
loadFn = ()=>{
|
|
188
|
+
element.removeEventListener('load', loadFn);
|
|
189
|
+
element.removeEventListener('error', errFn);
|
|
190
|
+
}
|
|
191
|
+
function loadFn() {
|
|
182
192
|
finishScriptLoading();
|
|
183
|
-
resolve(
|
|
184
|
-
}
|
|
185
|
-
errFn
|
|
193
|
+
resolve();
|
|
194
|
+
}
|
|
195
|
+
function errFn(ev) {
|
|
186
196
|
finishScriptLoading();
|
|
197
|
+
registry.delete(url);
|
|
198
|
+
element.remove();
|
|
187
199
|
const msg = `Failed to load script from ${url}`;
|
|
188
200
|
console.error(msg, ev);
|
|
189
201
|
reject(ev.message ?? msg);
|
|
190
|
-
}
|
|
202
|
+
}
|
|
191
203
|
element.addEventListener('load', loadFn);
|
|
192
204
|
element.addEventListener('error', errFn);
|
|
193
205
|
document.head.appendChild(element);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
finishScriptLoading = ()=>{
|
|
198
|
-
if (loadFn) {
|
|
199
|
-
scriptElement.removeEventListener('load', loadFn);
|
|
200
|
-
}
|
|
201
|
-
if (errFn) {
|
|
202
|
-
scriptElement.removeEventListener('error', errFn);
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
loadFn = ()=>{
|
|
206
|
-
finishScriptLoading();
|
|
207
|
-
resolve(null);
|
|
208
|
-
};
|
|
209
|
-
// this errFn does not log anything
|
|
210
|
-
errFn = (ev)=>{
|
|
211
|
-
finishScriptLoading();
|
|
212
|
-
reject(ev.message);
|
|
213
|
-
};
|
|
214
|
-
scriptElement.addEventListener('load', loadFn);
|
|
215
|
-
scriptElement.addEventListener('error', errFn);
|
|
216
|
-
} else {
|
|
217
|
-
console.warn(`Script at ${url} already loaded. Not loading it again.`);
|
|
218
|
-
resolve(null);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
206
|
+
});
|
|
207
|
+
registry.set(url, request);
|
|
208
|
+
return request;
|
|
221
209
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-dynamic-loading",
|
|
3
|
-
"version": "10.0.1-pre.
|
|
3
|
+
"version": "10.0.1-pre.5316",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Utilities for dynamically loading code in OpenMRS",
|
|
6
6
|
"type": "module",
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@openmrs/esm-globals": "^10.0.1-pre.
|
|
58
|
-
"@openmrs/esm-translations": "^10.0.1-pre.
|
|
57
|
+
"@openmrs/esm-globals": "^10.0.1-pre.5316",
|
|
58
|
+
"@openmrs/esm-translations": "^10.0.1-pre.5316"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@openmrs/esm-globals": "10.0.1-pre.
|
|
62
|
-
"@openmrs/esm-translations": "10.0.1-pre.
|
|
61
|
+
"@openmrs/esm-globals": "10.0.1-pre.5316",
|
|
62
|
+
"@openmrs/esm-translations": "10.0.1-pre.5316",
|
|
63
63
|
"@swc/cli": "0.8.1",
|
|
64
64
|
"@swc/core": "1.15.21",
|
|
65
65
|
"@vitest/coverage-v8": "^4.1.2",
|
|
@@ -58,6 +58,7 @@ describe('dynamic-loading', () => {
|
|
|
58
58
|
beforeEach(() => {
|
|
59
59
|
localStorage.clear();
|
|
60
60
|
document.head.querySelectorAll('script').forEach((el) => el.remove());
|
|
61
|
+
delete (window as any)[Symbol.for('__openmrs_script_loading')];
|
|
61
62
|
(globalThis as any).__webpack_share_scopes__ = { default: {} };
|
|
62
63
|
(window as any).spaBase = '/openmrs/spa';
|
|
63
64
|
mockGetImportMapOverrideMap.mockReturnValue({ imports: {} });
|
|
@@ -128,7 +129,7 @@ describe('dynamic-loading', () => {
|
|
|
128
129
|
|
|
129
130
|
script.dispatchEvent(new Event('load'));
|
|
130
131
|
|
|
131
|
-
await expect(promise).resolves.
|
|
132
|
+
await expect(promise).resolves.toBeUndefined();
|
|
132
133
|
});
|
|
133
134
|
|
|
134
135
|
it('rejects when the script fails to load', async () => {
|
|
@@ -201,7 +202,7 @@ describe('dynamic-loading', () => {
|
|
|
201
202
|
|
|
202
203
|
script.dispatchEvent(new Event('load'));
|
|
203
204
|
|
|
204
|
-
await expect(promise).resolves.
|
|
205
|
+
await expect(promise).resolves.toBeUndefined();
|
|
205
206
|
expect(mockGetCurrentPageMap).not.toHaveBeenCalled();
|
|
206
207
|
});
|
|
207
208
|
|
|
@@ -216,24 +217,89 @@ describe('dynamic-loading', () => {
|
|
|
216
217
|
expect(script).not.toBeNull();
|
|
217
218
|
script.dispatchEvent(new Event('load'));
|
|
218
219
|
|
|
219
|
-
await expect(promise).resolves.
|
|
220
|
+
await expect(promise).resolves.toBeUndefined();
|
|
220
221
|
});
|
|
221
222
|
|
|
222
|
-
it('does not
|
|
223
|
+
it('does not re-request a script that has already loaded', async () => {
|
|
223
224
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
224
225
|
mockGetCurrentPageMap.mockResolvedValue({
|
|
225
226
|
imports: { '@openmrs/esm-foo': 'http://localhost/foo.js' },
|
|
226
227
|
});
|
|
227
228
|
|
|
228
|
-
// First load
|
|
229
229
|
const firstPromise = preloadImport('@openmrs/esm-foo');
|
|
230
230
|
const script = await waitForScript('http://localhost/foo.js');
|
|
231
231
|
script.dispatchEvent(new Event('load'));
|
|
232
232
|
await firstPromise;
|
|
233
233
|
|
|
234
|
-
//
|
|
235
|
-
await expect(preloadImport('@openmrs/esm-foo')).resolves.
|
|
234
|
+
// the package defines no container global, so this is decided by the script registry alone
|
|
235
|
+
await expect(preloadImport('@openmrs/esm-foo')).resolves.toBeUndefined();
|
|
236
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
237
|
+
expect(document.head.querySelectorAll('script[src="http://localhost/foo.js"]')).toHaveLength(1);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('shares the request with a caller that reaches it after the script loaded', async () => {
|
|
241
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
242
|
+
const importMap = { imports: { '@openmrs/esm-foo': 'http://localhost/foo.js' } };
|
|
243
|
+
|
|
244
|
+
let releaseSecondMap!: () => void;
|
|
245
|
+
const secondMap = new Promise((resolve) => {
|
|
246
|
+
releaseSecondMap = () => resolve(importMap);
|
|
247
|
+
});
|
|
248
|
+
mockGetCurrentPageMap.mockResolvedValueOnce(importMap).mockReturnValueOnce(secondMap);
|
|
249
|
+
|
|
250
|
+
const first = preloadImport('@openmrs/esm-foo');
|
|
251
|
+
const second = preloadImport('@openmrs/esm-foo');
|
|
252
|
+
|
|
253
|
+
const script = await waitForScript('http://localhost/foo.js');
|
|
254
|
+
script.dispatchEvent(new Event('load'));
|
|
255
|
+
await first;
|
|
256
|
+
|
|
257
|
+
// the second caller only learns the URL now, long after the load event it needed to see
|
|
258
|
+
releaseSecondMap();
|
|
259
|
+
|
|
260
|
+
await expect(second).resolves.toBeUndefined();
|
|
261
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
262
|
+
expect(document.head.querySelectorAll('script[src="http://localhost/foo.js"]')).toHaveLength(1);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('warns about, and does not re-request, a script it did not load itself', async () => {
|
|
266
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
267
|
+
mockGetCurrentPageMap.mockResolvedValue({
|
|
268
|
+
imports: { '@openmrs/esm-foo': 'http://localhost/foo.js' },
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const foreign = document.createElement('script');
|
|
272
|
+
foreign.src = 'http://localhost/foo.js';
|
|
273
|
+
document.head.appendChild(foreign);
|
|
274
|
+
|
|
275
|
+
await expect(preloadImport('@openmrs/esm-foo')).resolves.toBeUndefined();
|
|
236
276
|
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('already loaded'));
|
|
277
|
+
expect(document.head.querySelectorAll('script[src="http://localhost/foo.js"]')).toHaveLength(1);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('retries a script that failed to load', async () => {
|
|
281
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
282
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
283
|
+
mockGetCurrentPageMap.mockResolvedValue({
|
|
284
|
+
imports: { '@openmrs/esm-foo': 'http://localhost/foo.js' },
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const firstPromise = preloadImport('@openmrs/esm-foo');
|
|
288
|
+
const failed = await waitForScript('http://localhost/foo.js');
|
|
289
|
+
failed.dispatchEvent(new ErrorEvent('error', { message: 'net::ERR_FAILED' }));
|
|
290
|
+
await expect(firstPromise).rejects.toBe('net::ERR_FAILED');
|
|
291
|
+
|
|
292
|
+
// the dead element is gone, so the retry is a real request rather than a false success
|
|
293
|
+
expect(document.head.querySelector('script[src="http://localhost/foo.js"]')).toBeNull();
|
|
294
|
+
|
|
295
|
+
const secondPromise = preloadImport('@openmrs/esm-foo');
|
|
296
|
+
const retried = await waitForScript('http://localhost/foo.js');
|
|
297
|
+
expect(retried).not.toBe(failed);
|
|
298
|
+
|
|
299
|
+
retried.dispatchEvent(new Event('load'));
|
|
300
|
+
|
|
301
|
+
await expect(secondPromise).resolves.toBeUndefined();
|
|
302
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
237
303
|
});
|
|
238
304
|
|
|
239
305
|
it('resolves both callers when the same script is preloaded concurrently', async () => {
|
|
@@ -248,8 +314,8 @@ describe('dynamic-loading', () => {
|
|
|
248
314
|
const script = await waitForScript('http://localhost/foo.js');
|
|
249
315
|
script.dispatchEvent(new Event('load'));
|
|
250
316
|
|
|
251
|
-
await expect(first).resolves.
|
|
252
|
-
await expect(second).resolves.
|
|
317
|
+
await expect(first).resolves.toBeUndefined();
|
|
318
|
+
await expect(second).resolves.toBeUndefined();
|
|
253
319
|
});
|
|
254
320
|
|
|
255
321
|
it('rejects both callers when a concurrently-loaded script fails', async () => {
|
package/src/dynamic-loading.ts
CHANGED
|
@@ -109,7 +109,8 @@ function humanReadableMs(ms: number) {
|
|
|
109
109
|
* @internal
|
|
110
110
|
*
|
|
111
111
|
* This internal method is used to ensure that the script belonging
|
|
112
|
-
* to the given package is loaded and added to the head.
|
|
112
|
+
* to the given package is loaded and added to the head. Concurrent and
|
|
113
|
+
* repeat calls for the same package share a single request for it.
|
|
113
114
|
*
|
|
114
115
|
* @param jsPackage The package to load
|
|
115
116
|
* @param importMap The import map to use for loading this package.
|
|
@@ -140,9 +141,7 @@ export async function preloadImport(jsPackage: string, importMap?: ImportMap) {
|
|
|
140
141
|
|
|
141
142
|
const isOverridden = jsPackage in getImportMapOverrideMap().imports;
|
|
142
143
|
try {
|
|
143
|
-
return await
|
|
144
|
-
loadScript(url, resolve, reject);
|
|
145
|
-
});
|
|
144
|
+
return await loadScript(url);
|
|
146
145
|
} catch (err: any) {
|
|
147
146
|
if (isOverridden) {
|
|
148
147
|
dispatchToastShown({
|
|
@@ -198,99 +197,81 @@ function isFederatedModule(a: unknown): a is FederatedModule {
|
|
|
198
197
|
}
|
|
199
198
|
|
|
200
199
|
// internals to track script loading
|
|
201
|
-
|
|
202
|
-
// we
|
|
203
|
-
|
|
200
|
+
|
|
201
|
+
// if we're already loading a script, we should wait until the script is loaded
|
|
202
|
+
// we use a global to track this; we use `Symbol.for()` in case this gets loaded
|
|
203
|
+
// twice
|
|
204
|
+
const OPENMRS_SCRIPT_LOADING = Symbol.for('__openmrs_script_loading');
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The scripts this loader has requested, keyed by URL. A pending entry is in flight and a settled
|
|
208
|
+
* one has loaded; failed requests are removed, so any entry can be awaited rather than re-requested.
|
|
209
|
+
*/
|
|
210
|
+
function getScriptRegistry(): Map<string, Promise<void>> {
|
|
211
|
+
return (window[OPENMRS_SCRIPT_LOADING] ??= new Map<string, Promise<void>>());
|
|
212
|
+
}
|
|
204
213
|
|
|
205
214
|
/**
|
|
206
|
-
* Appends a `<script>` to the DOM with the given URL
|
|
215
|
+
* Appends a `<script>` to the DOM with the given URL, unless it has been requested already, and
|
|
216
|
+
* resolves once it has loaded. Callers arriving at any point in a script's lifecycle share one
|
|
217
|
+
* request for it.
|
|
218
|
+
*
|
|
219
|
+
* A script which fails loading is removed from the DOM and can be retried. A script which throws
|
|
220
|
+
* when loading, however, will remain in the DOM as the script loaded.
|
|
207
221
|
*/
|
|
208
|
-
function loadScript(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
222
|
+
function loadScript(url: string): Promise<void> {
|
|
223
|
+
const registry = getScriptRegistry();
|
|
224
|
+
|
|
225
|
+
const requested = registry.get(url);
|
|
226
|
+
if (requested) {
|
|
227
|
+
return requested;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (document.head.querySelector(`script[src="${url}"]`)) {
|
|
231
|
+
// something other than this loader put the script in the document, so assume it's usable
|
|
232
|
+
console.warn(`Script at ${url} already loaded. Not loading it again.`);
|
|
233
|
+
return Promise.resolve();
|
|
217
234
|
}
|
|
218
235
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
element.type = 'text/javascript';
|
|
224
|
-
element.async = true;
|
|
236
|
+
const element = document.createElement('script');
|
|
237
|
+
element.src = url;
|
|
238
|
+
element.type = 'text/javascript';
|
|
239
|
+
element.async = true;
|
|
225
240
|
|
|
226
|
-
|
|
241
|
+
const request = new Promise<void>((resolve, reject) => {
|
|
242
|
+
// displays an error if a script takes more than 5 seconds to load
|
|
227
243
|
const loadTimeout = setTimeout(() => {
|
|
228
244
|
console.error(
|
|
229
245
|
`The script at ${url} did not load within 5 seconds. This may indicate an issue with the imports in the script's entry-point file or with the script's bundler configuration.`,
|
|
230
246
|
);
|
|
231
247
|
}, 5_000); // 5 seconds; this is arbitrary
|
|
232
248
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
finishScriptLoading = () => {
|
|
249
|
+
function finishScriptLoading() {
|
|
236
250
|
clearTimeout(loadTimeout);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
element.removeEventListener('load', loadFn);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (errFn) {
|
|
244
|
-
element.removeEventListener('error', errFn);
|
|
245
|
-
}
|
|
246
|
-
};
|
|
251
|
+
element.removeEventListener('load', loadFn);
|
|
252
|
+
element.removeEventListener('error', errFn);
|
|
253
|
+
}
|
|
247
254
|
|
|
248
|
-
loadFn
|
|
255
|
+
function loadFn() {
|
|
249
256
|
finishScriptLoading();
|
|
250
|
-
resolve(
|
|
251
|
-
}
|
|
257
|
+
resolve();
|
|
258
|
+
}
|
|
252
259
|
|
|
253
|
-
errFn
|
|
260
|
+
function errFn(ev: ErrorEvent) {
|
|
254
261
|
finishScriptLoading();
|
|
262
|
+
registry.delete(url);
|
|
263
|
+
element.remove();
|
|
255
264
|
const msg = `Failed to load script from ${url}`;
|
|
256
265
|
console.error(msg, ev);
|
|
257
266
|
reject(ev.message ?? msg);
|
|
258
|
-
}
|
|
267
|
+
}
|
|
259
268
|
|
|
260
269
|
element.addEventListener('load', loadFn);
|
|
261
270
|
element.addEventListener('error', errFn);
|
|
262
271
|
|
|
263
272
|
document.head.appendChild(element);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
finishScriptLoading = () => {
|
|
269
|
-
if (loadFn) {
|
|
270
|
-
scriptElement.removeEventListener('load', loadFn);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (errFn) {
|
|
274
|
-
scriptElement.removeEventListener('error', errFn);
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
loadFn = () => {
|
|
279
|
-
finishScriptLoading();
|
|
280
|
-
resolve(null);
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
// this errFn does not log anything
|
|
284
|
-
errFn = (ev: ErrorEvent) => {
|
|
285
|
-
finishScriptLoading();
|
|
286
|
-
reject(ev.message);
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
scriptElement.addEventListener('load', loadFn);
|
|
290
|
-
scriptElement.addEventListener('error', errFn);
|
|
291
|
-
} else {
|
|
292
|
-
console.warn(`Script at ${url} already loaded. Not loading it again.`);
|
|
293
|
-
resolve(null);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
registry.set(url, request);
|
|
276
|
+
return request;
|
|
296
277
|
}
|