@servicetitan/web-components 39.1.0 → 39.3.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/common.d.ts +1 -0
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +1 -1
- package/dist/common.js.map +1 -1
- package/dist/utils/load-scripts.d.ts.map +1 -1
- package/dist/utils/load-scripts.js +18 -2
- package/dist/utils/load-scripts.js.map +1 -1
- package/package.json +12 -13
- package/src/__tests__/loader.test.tsx +82 -44
- package/src/common.ts +1 -1
- package/src/utils/__tests__/load-scripts.test.ts +172 -0
- package/src/utils/load-scripts.ts +19 -2
package/dist/common.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ interface GetJsonOptions extends RequestInit {
|
|
|
38
38
|
retryDelay?: number;
|
|
39
39
|
onRequestError?(error: GetJsonError): void;
|
|
40
40
|
}
|
|
41
|
+
export declare function createNetworkError(message: string): Error;
|
|
41
42
|
export declare function getJson<T = any>(url: string, { retries, retryDelay, onRequestError, ...config }?: GetJsonOptions): Promise<{
|
|
42
43
|
data: T;
|
|
43
44
|
headers: Headers;
|
package/dist/common.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,oCAAoC,CAAC;AACpE,OAAO,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAC;AAEhD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,2BAA2B;IACxC,YAAY,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,SAAS,CAAA;KAAE,CAAC;CAC7D;AAED,eAAO,MAAM,cAAc,uDAAwC,CAAC;AACpE,eAAO,MAAM,0BAA0B,oEAEtC,CAAC;AACF,eAAO,MAAM,mCAAmC,4EAE/C,CAAC;AACF,eAAO,MAAM,sBAAsB,wDAAiD,CAAC;AACrF,eAAO,MAAM,iBAAiB,2DAA+C,CAAC;AAE9E,MAAM,WAAW,OAAO;IACpB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,2BAA2B,CAAC,EAAE,2BAA2B,CAAC;IAC1D,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC1B,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,cAAe,SAAQ,WAAW;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CAC9C;
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,oCAAoC,CAAC;AACpE,OAAO,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAC;AAEhD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,2BAA2B;IACxC,YAAY,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,SAAS,CAAA;KAAE,CAAC;CAC7D;AAED,eAAO,MAAM,cAAc,uDAAwC,CAAC;AACpE,eAAO,MAAM,0BAA0B,oEAEtC,CAAC;AACF,eAAO,MAAM,mCAAmC,4EAE/C,CAAC;AACF,eAAO,MAAM,sBAAsB,wDAAiD,CAAC;AACrF,eAAO,MAAM,iBAAiB,2DAA+C,CAAC;AAE9E,MAAM,WAAW,OAAO;IACpB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,2BAA2B,CAAC,EAAE,2BAA2B,CAAC;IAC1D,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC1B,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,cAAe,SAAQ,WAAW;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CAC9C;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,SAMjD;AAED,wBAAsB,OAAO,CAAC,CAAC,GAAG,GAAG,EACjC,GAAG,EAAE,MAAM,EACX,EAAE,OAAW,EAAE,UAAiB,EAAE,cAAc,EAAE,GAAG,MAAM,EAAE,GAAE,cAAmB,GACnF,OAAO,CAAC;IAAE,IAAI,EAAE,CAAC,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAiDrD"}
|
package/dist/common.js
CHANGED
|
@@ -4,7 +4,7 @@ export const EXPOSED_DEPENDENCIES_TOKEN = symbolToken('EXPOSED_DEPENDENCIES_TOKE
|
|
|
4
4
|
export const EXPOSED_INSTANCE_DEPENDENCIES_TOKEN = symbolToken('EXPOSED_INSTANCE_DEPENDENCIES_TOKEN');
|
|
5
5
|
export const IS_WEB_COMPONENT_TOKEN = symbolToken('IS_WEB_COMPONENT_TOKEN');
|
|
6
6
|
export const RENDER_ROOT_TOKEN = symbolToken('RENDER_ROOT_TOKEN');
|
|
7
|
-
function createNetworkError(message) {
|
|
7
|
+
export function createNetworkError(message) {
|
|
8
8
|
const error = new Error(message);
|
|
9
9
|
error.name = 'NetworkError';
|
|
10
10
|
return error;
|
package/dist/common.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/common.ts"],"sourcesContent":["import { type LDService } from '@servicetitan/launchdarkly-service';\nimport { Log } from '@servicetitan/log-service';\nimport { symbolToken } from '@servicetitan/react-ioc';\nimport { ExposedDependencies } from '@servicetitan/startup-utils';\nimport { EventBus } from './event-bus';\nimport { HistoryManager } from './history-manager';\n\nexport interface ExposedInstanceDependencies {\n launchDarkly?: { version: string; ldService?: LDService };\n}\n\nexport const BASENAME_TOKEN = symbolToken<string>('BASENAME_TOKEN');\nexport const EXPOSED_DEPENDENCIES_TOKEN = symbolToken<ExposedDependencies>(\n 'EXPOSED_DEPENDENCIES_TOKEN'\n);\nexport const EXPOSED_INSTANCE_DEPENDENCIES_TOKEN = symbolToken<ExposedInstanceDependencies>(\n 'EXPOSED_INSTANCE_DEPENDENCIES_TOKEN'\n);\nexport const IS_WEB_COMPONENT_TOKEN = symbolToken<boolean>('IS_WEB_COMPONENT_TOKEN');\nexport const RENDER_ROOT_TOKEN = symbolToken<ShadowRoot>('RENDER_ROOT_TOKEN');\n\nexport interface Entries {\n ldService?: LDService;\n logService?: Log;\n historyManager?: HistoryManager;\n exposedDependencies?: ExposedDependencies;\n exposedInstanceDependencies?: ExposedInstanceDependencies;\n eventBus?: EventBus;\n basename?: string;\n onReady?: () => void;\n onDispose?: () => void;\n}\n\nexport interface IWebComponent {\n provide(entries: Entries): void;\n}\n\nexport interface GetJsonError {\n status: number;\n body: string;\n json: object;\n}\n\ninterface GetJsonOptions extends RequestInit {\n retries?: number;\n retryDelay?: number;\n onRequestError?(error: GetJsonError): void;\n}\n\
|
|
1
|
+
{"version":3,"sources":["../src/common.ts"],"sourcesContent":["import { type LDService } from '@servicetitan/launchdarkly-service';\nimport { Log } from '@servicetitan/log-service';\nimport { symbolToken } from '@servicetitan/react-ioc';\nimport { ExposedDependencies } from '@servicetitan/startup-utils';\nimport { EventBus } from './event-bus';\nimport { HistoryManager } from './history-manager';\n\nexport interface ExposedInstanceDependencies {\n launchDarkly?: { version: string; ldService?: LDService };\n}\n\nexport const BASENAME_TOKEN = symbolToken<string>('BASENAME_TOKEN');\nexport const EXPOSED_DEPENDENCIES_TOKEN = symbolToken<ExposedDependencies>(\n 'EXPOSED_DEPENDENCIES_TOKEN'\n);\nexport const EXPOSED_INSTANCE_DEPENDENCIES_TOKEN = symbolToken<ExposedInstanceDependencies>(\n 'EXPOSED_INSTANCE_DEPENDENCIES_TOKEN'\n);\nexport const IS_WEB_COMPONENT_TOKEN = symbolToken<boolean>('IS_WEB_COMPONENT_TOKEN');\nexport const RENDER_ROOT_TOKEN = symbolToken<ShadowRoot>('RENDER_ROOT_TOKEN');\n\nexport interface Entries {\n ldService?: LDService;\n logService?: Log;\n historyManager?: HistoryManager;\n exposedDependencies?: ExposedDependencies;\n exposedInstanceDependencies?: ExposedInstanceDependencies;\n eventBus?: EventBus;\n basename?: string;\n onReady?: () => void;\n onDispose?: () => void;\n}\n\nexport interface IWebComponent {\n provide(entries: Entries): void;\n}\n\nexport interface GetJsonError {\n status: number;\n body: string;\n json: object;\n}\n\ninterface GetJsonOptions extends RequestInit {\n retries?: number;\n retryDelay?: number;\n onRequestError?(error: GetJsonError): void;\n}\n\nexport function createNetworkError(message: string) {\n const error = new Error(message);\n\n error.name = 'NetworkError';\n\n return error;\n}\n\nexport async function getJson<T = any>(\n url: string,\n { retries = 5, retryDelay = 1000, onRequestError, ...config }: GetJsonOptions = {}\n): Promise<{ data: T; headers: Headers; url: string }> {\n let response: Response;\n try {\n response = await fetch(url, config);\n } catch {\n if (retries > 0 && !config.signal?.aborted) {\n return new Promise((resolve, reject) => {\n const { signal } = config;\n\n const timeoutId = window.setTimeout(() => {\n getJson(url, {\n ...config,\n onRequestError,\n retries: retries - 1,\n retryDelay: retryDelay * 4,\n }).then(resolve, reject);\n }, retryDelay);\n\n const abortHandler = () => {\n window.clearTimeout(timeoutId);\n signal?.removeEventListener('abort', abortHandler);\n reject(signal?.reason);\n };\n\n signal?.addEventListener('abort', abortHandler);\n });\n }\n\n throw createNetworkError(`Failed to fetch: \"${url}\".`);\n }\n\n const responseJSON = await response.json().catch(() => ({}));\n\n if (!response.ok) {\n let message = `Requesting \"${url}\" failed with status code ${response.status}.`;\n\n try {\n message += ` Details: ${await response.text()}.`;\n } catch {} // eslint-disable-line no-empty\n\n onRequestError?.({\n status: response.status,\n json: responseJSON,\n body: await response.text().catch(() => ''),\n });\n throw createNetworkError(message);\n }\n\n return { data: responseJSON, headers: response.headers, url: response.url ?? url };\n}\n"],"names":["symbolToken","BASENAME_TOKEN","EXPOSED_DEPENDENCIES_TOKEN","EXPOSED_INSTANCE_DEPENDENCIES_TOKEN","IS_WEB_COMPONENT_TOKEN","RENDER_ROOT_TOKEN","createNetworkError","message","error","Error","name","getJson","url","retries","retryDelay","onRequestError","config","response","fetch","signal","aborted","Promise","resolve","reject","timeoutId","window","setTimeout","then","abortHandler","clearTimeout","removeEventListener","reason","addEventListener","responseJSON","json","catch","ok","status","text","body","data","headers"],"mappings":"AAEA,SAASA,WAAW,QAAQ,0BAA0B;AAStD,OAAO,MAAMC,iBAAiBD,YAAoB,kBAAkB;AACpE,OAAO,MAAME,6BAA6BF,YACtC,8BACF;AACF,OAAO,MAAMG,sCAAsCH,YAC/C,uCACF;AACF,OAAO,MAAMI,yBAAyBJ,YAAqB,0BAA0B;AACrF,OAAO,MAAMK,oBAAoBL,YAAwB,qBAAqB;AA8B9E,OAAO,SAASM,mBAAmBC,OAAe;IAC9C,MAAMC,QAAQ,IAAIC,MAAMF;IAExBC,MAAME,IAAI,GAAG;IAEb,OAAOF;AACX;AAEA,OAAO,eAAeG,QAClBC,GAAW,EACX,EAAEC,UAAU,CAAC,EAAEC,aAAa,IAAI,EAAEC,cAAc,EAAE,GAAGC,QAAwB,GAAG,CAAC,CAAC;QAiDrBC;IA/C7D,IAAIA;IACJ,IAAI;QACAA,WAAW,MAAMC,MAAMN,KAAKI;IAChC,EAAE,eAAM;YACgBA;QAApB,IAAIH,UAAU,KAAK,GAACG,iBAAAA,OAAOG,MAAM,cAAbH,qCAAAA,eAAeI,OAAO,GAAE;YACxC,OAAO,IAAIC,QAAQ,CAACC,SAASC;gBACzB,MAAM,EAAEJ,MAAM,EAAE,GAAGH;gBAEnB,MAAMQ,YAAYC,OAAOC,UAAU,CAAC;oBAChCf,QAAQC,KAAK;wBACT,GAAGI,MAAM;wBACTD;wBACAF,SAASA,UAAU;wBACnBC,YAAYA,aAAa;oBAC7B,GAAGa,IAAI,CAACL,SAASC;gBACrB,GAAGT;gBAEH,MAAMc,eAAe;oBACjBH,OAAOI,YAAY,CAACL;oBACpBL,mBAAAA,6BAAAA,OAAQW,mBAAmB,CAAC,SAASF;oBACrCL,OAAOJ,mBAAAA,6BAAAA,OAAQY,MAAM;gBACzB;gBAEAZ,mBAAAA,6BAAAA,OAAQa,gBAAgB,CAAC,SAASJ;YACtC;QACJ;QAEA,MAAMtB,mBAAmB,CAAC,kBAAkB,EAAEM,IAAI,EAAE,CAAC;IACzD;IAEA,MAAMqB,eAAe,MAAMhB,SAASiB,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;IAEzD,IAAI,CAAClB,SAASmB,EAAE,EAAE;QACd,IAAI7B,UAAU,CAAC,YAAY,EAAEK,IAAI,0BAA0B,EAAEK,SAASoB,MAAM,CAAC,CAAC,CAAC;QAE/E,IAAI;YACA9B,WAAW,CAAC,UAAU,EAAE,MAAMU,SAASqB,IAAI,GAAG,CAAC,CAAC;QACpD,EAAE,eAAM,CAAC,EAAE,+BAA+B;QAE1CvB,2BAAAA,qCAAAA,eAAiB;YACbsB,QAAQpB,SAASoB,MAAM;YACvBH,MAAMD;YACNM,MAAM,MAAMtB,SAASqB,IAAI,GAAGH,KAAK,CAAC,IAAM;QAC5C;QACA,MAAM7B,mBAAmBC;IAC7B;IAEA,OAAO;QAAEiC,MAAMP;QAAcQ,SAASxB,SAASwB,OAAO;QAAE7B,GAAG,GAAEK,gBAAAA,SAASL,GAAG,cAAZK,2BAAAA,gBAAgBL;IAAI;AACrF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-scripts.d.ts","sourceRoot":"","sources":["../../src/utils/load-scripts.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"load-scripts.d.ts","sourceRoot":"","sources":["../../src/utils/load-scripts.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,UAAU,mBAwDrE"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { createNetworkError } from '../common';
|
|
1
2
|
export function loadScripts({ urls, WebComponent, module }) {
|
|
2
3
|
return Promise.all(urls.js.map((url)=>{
|
|
3
4
|
const script = Array.from(document.scripts).find(({ src })=>src === url);
|
|
4
5
|
if (!script) {
|
|
5
|
-
return new Promise((resolve)=>{
|
|
6
|
+
return new Promise((resolve, reject)=>{
|
|
6
7
|
const script = document.createElement('script');
|
|
7
8
|
if (module) {
|
|
8
9
|
script.type = 'module';
|
|
@@ -13,20 +14,35 @@ export function loadScripts({ urls, WebComponent, module }) {
|
|
|
13
14
|
script.onload = ()=>{
|
|
14
15
|
script.dataset.webComponent = WebComponent;
|
|
15
16
|
script.onload = null;
|
|
17
|
+
script.onerror = null;
|
|
16
18
|
resolve();
|
|
17
19
|
};
|
|
20
|
+
script.onerror = ()=>{
|
|
21
|
+
/*
|
|
22
|
+
* Remove the failed script so a later remount can retry
|
|
23
|
+
* instead of waiting on it forever.
|
|
24
|
+
*/ script.remove();
|
|
25
|
+
reject(createNetworkError(`Failed to load script: "${url}".`));
|
|
26
|
+
};
|
|
18
27
|
script.src = url;
|
|
19
28
|
document.body.append(script);
|
|
20
29
|
});
|
|
21
30
|
}
|
|
22
31
|
if (!script.dataset.webComponent) {
|
|
23
|
-
return new Promise((resolve)=>{
|
|
32
|
+
return new Promise((resolve, reject)=>{
|
|
24
33
|
const originalOnload = script.onload;
|
|
34
|
+
const originalOnerror = script.onerror;
|
|
25
35
|
script.onload = (...args)=>{
|
|
26
36
|
originalOnload === null || originalOnload === void 0 ? void 0 : originalOnload.call(script, ...args);
|
|
27
37
|
script.onload = null;
|
|
38
|
+
script.onerror = null;
|
|
28
39
|
resolve();
|
|
29
40
|
};
|
|
41
|
+
script.onerror = (...args)=>{
|
|
42
|
+
originalOnerror === null || originalOnerror === void 0 ? void 0 : originalOnerror.call(script, ...args);
|
|
43
|
+
script.remove();
|
|
44
|
+
reject(createNetworkError(`Failed to load script: "${url}".`));
|
|
45
|
+
};
|
|
30
46
|
});
|
|
31
47
|
}
|
|
32
48
|
return Promise.resolve();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/load-scripts.ts"],"sourcesContent":["import { BundleInfo } from './get-bundle-info';\n\nexport function loadScripts({ urls, WebComponent, module }: BundleInfo) {\n return Promise.all(\n urls.js.map(url => {\n const script = Array.from(document.scripts).find(({ src }) => src === url);\n if (!script) {\n return new Promise<void>(resolve => {\n const script = document.createElement('script');\n\n if (module) {\n script.type = 'module';\n } else {\n script.async = true;\n }\n\n script.crossOrigin = 'anonymous';\n script.onload = () => {\n script.dataset.webComponent = WebComponent;\n script.onload = null;\n resolve();\n };\n script.src = url;\n\n document.body.append(script);\n });\n }\n\n if (!script.dataset.webComponent) {\n return new Promise<void>(resolve => {\n const originalOnload = script.onload;\n script.onload = (...args) => {\n originalOnload?.call(script, ...args);\n script.onload = null;\n resolve();\n };\n });\n }\n\n return Promise.resolve();\n })\n );\n}\n"],"names":["loadScripts","urls","WebComponent","module","Promise","all","js","map","url","script","Array","from","document","scripts","find","src","resolve","createElement","type","async","crossOrigin","onload","dataset","webComponent","body","append","originalOnload","args","call"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/utils/load-scripts.ts"],"sourcesContent":["import { createNetworkError } from '../common';\nimport { BundleInfo } from './get-bundle-info';\n\nexport function loadScripts({ urls, WebComponent, module }: BundleInfo) {\n return Promise.all(\n urls.js.map(url => {\n const script = Array.from(document.scripts).find(({ src }) => src === url);\n if (!script) {\n return new Promise<void>((resolve, reject) => {\n const script = document.createElement('script');\n\n if (module) {\n script.type = 'module';\n } else {\n script.async = true;\n }\n\n script.crossOrigin = 'anonymous';\n script.onload = () => {\n script.dataset.webComponent = WebComponent;\n script.onload = null;\n script.onerror = null;\n resolve();\n };\n script.onerror = () => {\n /*\n * Remove the failed script so a later remount can retry\n * instead of waiting on it forever.\n */\n script.remove();\n reject(createNetworkError(`Failed to load script: \"${url}\".`));\n };\n script.src = url;\n\n document.body.append(script);\n });\n }\n\n if (!script.dataset.webComponent) {\n return new Promise<void>((resolve, reject) => {\n const originalOnload = script.onload;\n const originalOnerror = script.onerror;\n script.onload = (...args) => {\n originalOnload?.call(script, ...args);\n script.onload = null;\n script.onerror = null;\n resolve();\n };\n script.onerror = (...args) => {\n originalOnerror?.call(script, ...args);\n script.remove();\n reject(createNetworkError(`Failed to load script: \"${url}\".`));\n };\n });\n }\n\n return Promise.resolve();\n })\n );\n}\n"],"names":["createNetworkError","loadScripts","urls","WebComponent","module","Promise","all","js","map","url","script","Array","from","document","scripts","find","src","resolve","reject","createElement","type","async","crossOrigin","onload","dataset","webComponent","onerror","remove","body","append","originalOnload","originalOnerror","args","call"],"mappings":"AAAA,SAASA,kBAAkB,QAAQ,YAAY;AAG/C,OAAO,SAASC,YAAY,EAAEC,IAAI,EAAEC,YAAY,EAAEC,MAAM,EAAc;IAClE,OAAOC,QAAQC,GAAG,CACdJ,KAAKK,EAAE,CAACC,GAAG,CAACC,CAAAA;QACR,MAAMC,SAASC,MAAMC,IAAI,CAACC,SAASC,OAAO,EAAEC,IAAI,CAAC,CAAC,EAAEC,GAAG,EAAE,GAAKA,QAAQP;QACtE,IAAI,CAACC,QAAQ;YACT,OAAO,IAAIL,QAAc,CAACY,SAASC;gBAC/B,MAAMR,SAASG,SAASM,aAAa,CAAC;gBAEtC,IAAIf,QAAQ;oBACRM,OAAOU,IAAI,GAAG;gBAClB,OAAO;oBACHV,OAAOW,KAAK,GAAG;gBACnB;gBAEAX,OAAOY,WAAW,GAAG;gBACrBZ,OAAOa,MAAM,GAAG;oBACZb,OAAOc,OAAO,CAACC,YAAY,GAAGtB;oBAC9BO,OAAOa,MAAM,GAAG;oBAChBb,OAAOgB,OAAO,GAAG;oBACjBT;gBACJ;gBACAP,OAAOgB,OAAO,GAAG;oBACb;;;yBAGC,GACDhB,OAAOiB,MAAM;oBACbT,OAAOlB,mBAAmB,CAAC,wBAAwB,EAAES,IAAI,EAAE,CAAC;gBAChE;gBACAC,OAAOM,GAAG,GAAGP;gBAEbI,SAASe,IAAI,CAACC,MAAM,CAACnB;YACzB;QACJ;QAEA,IAAI,CAACA,OAAOc,OAAO,CAACC,YAAY,EAAE;YAC9B,OAAO,IAAIpB,QAAc,CAACY,SAASC;gBAC/B,MAAMY,iBAAiBpB,OAAOa,MAAM;gBACpC,MAAMQ,kBAAkBrB,OAAOgB,OAAO;gBACtChB,OAAOa,MAAM,GAAG,CAAC,GAAGS;oBAChBF,2BAAAA,qCAAAA,eAAgBG,IAAI,CAACvB,WAAWsB;oBAChCtB,OAAOa,MAAM,GAAG;oBAChBb,OAAOgB,OAAO,GAAG;oBACjBT;gBACJ;gBACAP,OAAOgB,OAAO,GAAG,CAAC,GAAGM;oBACjBD,4BAAAA,sCAAAA,gBAAiBE,IAAI,CAACvB,WAAWsB;oBACjCtB,OAAOiB,MAAM;oBACbT,OAAOlB,mBAAmB,CAAC,wBAAwB,EAAES,IAAI,EAAE,CAAC;gBAChE;YACJ;QACJ;QAEA,OAAOJ,QAAQY,OAAO;IAC1B;AAER"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@servicetitan/web-components",
|
|
3
|
-
"version": "39.
|
|
3
|
+
"version": "39.3.0",
|
|
4
4
|
"description": "Framework to build, register, and load Microfrontends into host applications",
|
|
5
5
|
"homepage": "https://docs.st.dev/docs/frontend/uikit/web-components",
|
|
6
6
|
"repository": {
|
|
@@ -31,21 +31,21 @@
|
|
|
31
31
|
"src"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@servicetitan/error-boundary": "39.
|
|
35
|
-
"@servicetitan/startup-utils": "39.
|
|
34
|
+
"@servicetitan/error-boundary": "39.3.0",
|
|
35
|
+
"@servicetitan/startup-utils": "39.3.0",
|
|
36
36
|
"lodash.get": "~4.4.2",
|
|
37
37
|
"mitt": "~3.0.1",
|
|
38
38
|
"semver": "~7.8.5"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@servicetitan/anvil2": "^3.
|
|
41
|
+
"@servicetitan/anvil2": "^3.13.3",
|
|
42
42
|
"@servicetitan/design-system": "~14.5.3",
|
|
43
|
-
"@servicetitan/launchdarkly-service": "39.
|
|
44
|
-
"@servicetitan/log-service": "39.
|
|
45
|
-
"@servicetitan/react-ioc": "39.
|
|
43
|
+
"@servicetitan/launchdarkly-service": "39.3.0",
|
|
44
|
+
"@servicetitan/log-service": "39.3.0",
|
|
45
|
+
"@servicetitan/react-ioc": "39.3.0",
|
|
46
46
|
"@testing-library/dom": "^10.4.1",
|
|
47
47
|
"@testing-library/jest-dom": "^7.0.1",
|
|
48
|
-
"@testing-library/react": "^16.3.
|
|
48
|
+
"@testing-library/react": "^16.3.3",
|
|
49
49
|
"@types/history": "~4.7.10",
|
|
50
50
|
"@types/lodash.get": "~4.4.6",
|
|
51
51
|
"@types/react": "~18.3.3",
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@servicetitan/anvil2": ">=1.5.0",
|
|
62
62
|
"@servicetitan/design-system": ">=14.5.3",
|
|
63
|
-
"@servicetitan/launchdarkly-service": "39.
|
|
64
|
-
"@servicetitan/log-service": "39.
|
|
65
|
-
"@servicetitan/react-ioc": "39.
|
|
63
|
+
"@servicetitan/launchdarkly-service": "39.3.0",
|
|
64
|
+
"@servicetitan/log-service": "39.3.0",
|
|
65
|
+
"@servicetitan/react-ioc": "39.3.0",
|
|
66
66
|
"history": "~4.10.1",
|
|
67
67
|
"react": ">=17.0.2",
|
|
68
68
|
"react-dom": ">=17.0.2"
|
|
@@ -94,6 +94,5 @@
|
|
|
94
94
|
"fallback": "empty"
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
}
|
|
98
|
-
"gitHead": "a2471578a506e8c40e90956a5c70cc1638bbe9c1"
|
|
97
|
+
}
|
|
99
98
|
}
|
|
@@ -48,6 +48,7 @@ describe('[web-components] Loader', () => {
|
|
|
48
48
|
|
|
49
49
|
let props: LoaderProps<any>;
|
|
50
50
|
let onloadRef: Function | null;
|
|
51
|
+
let onerrorRef: Function | null;
|
|
51
52
|
let singletons: ProviderProps['singletons'];
|
|
52
53
|
let getJsonMockFns: Record<string, Function>;
|
|
53
54
|
|
|
@@ -62,8 +63,8 @@ describe('[web-components] Loader', () => {
|
|
|
62
63
|
window.customElements.define(WEB_COMPONENT_NAME, WebComponent);
|
|
63
64
|
|
|
64
65
|
/*
|
|
65
|
-
* mock <script>'s onload so that we can call
|
|
66
|
-
* because
|
|
66
|
+
* mock <script>'s onload/onerror so that we can call them directly from the test,
|
|
67
|
+
* because they won't call themselves in the test environment
|
|
67
68
|
*/
|
|
68
69
|
Object.defineProperty(HTMLScriptElement.prototype, 'onload', {
|
|
69
70
|
get() {
|
|
@@ -74,6 +75,15 @@ describe('[web-components] Loader', () => {
|
|
|
74
75
|
this._onload = onload;
|
|
75
76
|
},
|
|
76
77
|
});
|
|
78
|
+
Object.defineProperty(HTMLScriptElement.prototype, 'onerror', {
|
|
79
|
+
get() {
|
|
80
|
+
return this._onerror;
|
|
81
|
+
},
|
|
82
|
+
set(onerror: Function) {
|
|
83
|
+
onerrorRef = onerror;
|
|
84
|
+
this._onerror = onerror;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
77
87
|
});
|
|
78
88
|
|
|
79
89
|
function initializeGetJsonMockFns(url = baseUrl) {
|
|
@@ -113,6 +123,7 @@ describe('[web-components] Loader', () => {
|
|
|
113
123
|
setStartupVersion('27.2.0');
|
|
114
124
|
props = { src: baseUrl, cache: false };
|
|
115
125
|
onloadRef = null;
|
|
126
|
+
onerrorRef = null;
|
|
116
127
|
singletons = [];
|
|
117
128
|
initializeGetJsonMockFns();
|
|
118
129
|
|
|
@@ -152,6 +163,52 @@ describe('[web-components] Loader', () => {
|
|
|
152
163
|
expect(common.getJson).toHaveBeenCalledTimes(1);
|
|
153
164
|
}
|
|
154
165
|
|
|
166
|
+
function itRendersErrorFallback(triggerFailure?: () => void | Promise<void>) {
|
|
167
|
+
beforeEach(() => {
|
|
168
|
+
jest.spyOn(console, 'error').mockImplementation(jest.fn); // suppress error boundary logging
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('renders default error fallback', async () => {
|
|
172
|
+
subject();
|
|
173
|
+
await triggerFailure?.();
|
|
174
|
+
|
|
175
|
+
await waitFor(() =>
|
|
176
|
+
expect(screen.getByText('Something went wrong')).toBeInTheDocument()
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe.each(localHosts)('when src contains "%s"', host => {
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
props.src = `http://${host}/mfe`;
|
|
183
|
+
initializeGetJsonMockFns(props.src);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('appends src to error fallback', async () => {
|
|
187
|
+
subject();
|
|
188
|
+
await triggerFailure?.();
|
|
189
|
+
|
|
190
|
+
await waitFor(() =>
|
|
191
|
+
expect(
|
|
192
|
+
screen.getByText(`Something went wrong loading ${props.src}`)
|
|
193
|
+
).toBeInTheDocument()
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('when an errorFallback is provided', () => {
|
|
199
|
+
beforeEach(() => {
|
|
200
|
+
props.errorFallback = <div>bar</div>;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('renders provided error fallback', async () => {
|
|
204
|
+
subject();
|
|
205
|
+
await triggerFailure?.();
|
|
206
|
+
|
|
207
|
+
await waitFor(() => expect(screen.getByText('bar')).toBeInTheDocument());
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
155
212
|
describe.each(localHosts)('when src contains "%s"', host => {
|
|
156
213
|
beforeEach(() => {
|
|
157
214
|
props.src = `http://${host}/mfe`;
|
|
@@ -211,49 +268,13 @@ describe('[web-components] Loader', () => {
|
|
|
211
268
|
|
|
212
269
|
describe('when the loadingFallback throws an error', () => {
|
|
213
270
|
beforeEach(() => {
|
|
214
|
-
jest.spyOn(console, 'error').mockImplementation(jest.fn); // suppress console output
|
|
215
271
|
const ThrowError: FC = () => {
|
|
216
272
|
throw new Error('woops');
|
|
217
273
|
};
|
|
218
274
|
props.loadingFallback = <ThrowError />;
|
|
219
275
|
});
|
|
220
276
|
|
|
221
|
-
|
|
222
|
-
subject();
|
|
223
|
-
|
|
224
|
-
await waitFor(() =>
|
|
225
|
-
expect(screen.getByText('Something went wrong')).toBeInTheDocument()
|
|
226
|
-
);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
describe.each(localHosts)('when src contains "%s"', host => {
|
|
230
|
-
beforeEach(() => {
|
|
231
|
-
props.src = `http://${host}/mfe`;
|
|
232
|
-
initializeGetJsonMockFns(props.src);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test('appends src to error fallback', async () => {
|
|
236
|
-
subject();
|
|
237
|
-
|
|
238
|
-
await waitFor(() =>
|
|
239
|
-
expect(
|
|
240
|
-
screen.getByText(`Something went wrong loading ${props.src}`)
|
|
241
|
-
).toBeInTheDocument()
|
|
242
|
-
);
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
describe('when an errorFallback is provided', () => {
|
|
247
|
-
beforeEach(() => {
|
|
248
|
-
props.errorFallback = <div>bar</div>;
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test('renders provided error fallback', async () => {
|
|
252
|
-
subject();
|
|
253
|
-
|
|
254
|
-
await waitFor(() => expect(screen.getByText('bar')).toBeInTheDocument());
|
|
255
|
-
});
|
|
256
|
-
});
|
|
277
|
+
itRendersErrorFallback();
|
|
257
278
|
});
|
|
258
279
|
|
|
259
280
|
describe('when loadingFallback is not delayed', () => {
|
|
@@ -863,11 +884,6 @@ describe('[web-components] Loader', () => {
|
|
|
863
884
|
document.body.appendChild(script);
|
|
864
885
|
});
|
|
865
886
|
|
|
866
|
-
test("the script tag's original onload is called", async () => {
|
|
867
|
-
await webComponentReadySetup();
|
|
868
|
-
expect(originalOnloadSpy).toHaveBeenCalled();
|
|
869
|
-
});
|
|
870
|
-
|
|
871
887
|
test('onload handler gets unassigned after it gets called', async () => {
|
|
872
888
|
await webComponentReadySetup();
|
|
873
889
|
expect(onloadRef).toBeNull();
|
|
@@ -980,6 +996,28 @@ describe('[web-components] Loader', () => {
|
|
|
980
996
|
});
|
|
981
997
|
});
|
|
982
998
|
|
|
999
|
+
describe('when the script tag fails to load', () => {
|
|
1000
|
+
beforeEach(() => {
|
|
1001
|
+
jest.spyOn(console, 'error').mockImplementation(jest.fn); // suppress error boundary logging
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
async function triggerScriptError() {
|
|
1005
|
+
await waitFor(() => onerrorRef !== null);
|
|
1006
|
+
act(() => {
|
|
1007
|
+
onerrorRef!();
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
itRendersErrorFallback(triggerScriptError);
|
|
1012
|
+
|
|
1013
|
+
test('does not hang on the loading fallback', async () => {
|
|
1014
|
+
subject();
|
|
1015
|
+
await triggerScriptError();
|
|
1016
|
+
|
|
1017
|
+
await waitFor(() => expect(screen.queryByText(/loading/i)).not.toBeInTheDocument());
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
|
|
983
1021
|
describe('when cache is enabled', () => {
|
|
984
1022
|
beforeAll(() => {
|
|
985
1023
|
jest.useFakeTimers();
|
package/src/common.ts
CHANGED
|
@@ -47,7 +47,7 @@ interface GetJsonOptions extends RequestInit {
|
|
|
47
47
|
onRequestError?(error: GetJsonError): void;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function createNetworkError(message: string) {
|
|
50
|
+
export function createNetworkError(message: string) {
|
|
51
51
|
const error = new Error(message);
|
|
52
52
|
|
|
53
53
|
error.name = 'NetworkError';
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { BundleInfo } from '../get-bundle-info';
|
|
3
|
+
import { loadScripts } from '../load-scripts';
|
|
4
|
+
|
|
5
|
+
describe('[web-components] loadScripts', () => {
|
|
6
|
+
const url = 'https://unpkg.servicetitan.com/mfe/dist/bundle/light/light.js';
|
|
7
|
+
const webComponentName = 'my-mfe';
|
|
8
|
+
|
|
9
|
+
let bundleInfo: BundleInfo;
|
|
10
|
+
let onloadRef: (() => void) | null;
|
|
11
|
+
let onerrorRef: (() => void) | null;
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
/*
|
|
15
|
+
* mock <script>'s onload/onerror so that we can call them directly from the test,
|
|
16
|
+
* because they won't call themselves in the test environment
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(HTMLScriptElement.prototype, 'onload', {
|
|
19
|
+
configurable: true,
|
|
20
|
+
get() {
|
|
21
|
+
return this._onload;
|
|
22
|
+
},
|
|
23
|
+
set(onload) {
|
|
24
|
+
onloadRef = onload;
|
|
25
|
+
this._onload = onload;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(HTMLScriptElement.prototype, 'onerror', {
|
|
29
|
+
configurable: true,
|
|
30
|
+
get() {
|
|
31
|
+
return this._onerror;
|
|
32
|
+
},
|
|
33
|
+
set(onerror) {
|
|
34
|
+
onerrorRef = onerror;
|
|
35
|
+
this._onerror = onerror;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
document.querySelectorAll('script').forEach(script => script.remove());
|
|
42
|
+
onloadRef = null;
|
|
43
|
+
onerrorRef = null;
|
|
44
|
+
bundleInfo = {
|
|
45
|
+
bundledWith: undefined,
|
|
46
|
+
cacheHit: undefined,
|
|
47
|
+
disposer: () => {},
|
|
48
|
+
mfeSupportsRerendering: false,
|
|
49
|
+
mfeHasUniqueChunkNamespace: false,
|
|
50
|
+
mfeHasCompatibleLaunchDarklyVersion: false,
|
|
51
|
+
mismatch: undefined,
|
|
52
|
+
resolvedBundleType: 'light',
|
|
53
|
+
urls: { js: [url], css: [] },
|
|
54
|
+
WebComponent: webComponentName,
|
|
55
|
+
version: undefined,
|
|
56
|
+
module: undefined,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const subject = () => loadScripts(bundleInfo);
|
|
61
|
+
|
|
62
|
+
function scriptTag() {
|
|
63
|
+
return document.querySelector(`script[src="${url}"]`) as HTMLScriptElement | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test('creates a script tag for the js url', () => {
|
|
67
|
+
subject();
|
|
68
|
+
|
|
69
|
+
expect(scriptTag()).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('sets crossOrigin to anonymous', () => {
|
|
73
|
+
subject();
|
|
74
|
+
|
|
75
|
+
expect(scriptTag()).toHaveAttribute('crossorigin', 'anonymous');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('when the script loads successfully', () => {
|
|
79
|
+
const subject = () => {
|
|
80
|
+
const promise = loadScripts(bundleInfo);
|
|
81
|
+
onloadRef!();
|
|
82
|
+
return promise;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
test('resolves', async () => {
|
|
86
|
+
await expect(subject()).resolves.toEqual([undefined]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('marks the script tag with the web component name', async () => {
|
|
90
|
+
await subject();
|
|
91
|
+
|
|
92
|
+
expect(scriptTag()).toHaveAttribute('data-web-component', webComponentName);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('unassigns the onload and onerror handlers', async () => {
|
|
96
|
+
await subject();
|
|
97
|
+
|
|
98
|
+
expect(scriptTag()!.onload).toBeNull();
|
|
99
|
+
expect(scriptTag()!.onerror).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('when the script fails to load', () => {
|
|
104
|
+
const subject = () => {
|
|
105
|
+
const promise = loadScripts(bundleInfo);
|
|
106
|
+
onerrorRef!();
|
|
107
|
+
return promise;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
test('rejects with a network error', async () => {
|
|
111
|
+
await expect(subject()).rejects.toThrow(`Failed to load script: "${url}".`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('removes the failed script tag from the DOM', async () => {
|
|
115
|
+
await subject().catch(() => {});
|
|
116
|
+
|
|
117
|
+
expect(scriptTag()).not.toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('when a script tag for this bundle already exists without a webComponent data attribute', () => {
|
|
122
|
+
let originalOnloadSpy: jest.Mock;
|
|
123
|
+
let originalOnerrorSpy: jest.Mock;
|
|
124
|
+
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
originalOnloadSpy = jest.fn();
|
|
127
|
+
originalOnerrorSpy = jest.fn();
|
|
128
|
+
|
|
129
|
+
const script = document.createElement('script');
|
|
130
|
+
script.src = url;
|
|
131
|
+
script.onload = originalOnloadSpy;
|
|
132
|
+
script.onerror = originalOnerrorSpy;
|
|
133
|
+
document.body.appendChild(script);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const subject = () => {
|
|
137
|
+
const promise = loadScripts(bundleInfo);
|
|
138
|
+
onloadRef!();
|
|
139
|
+
return promise;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
test('calls the original onload handler', async () => {
|
|
143
|
+
await subject();
|
|
144
|
+
|
|
145
|
+
expect(originalOnloadSpy).toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('when the script fails to load', () => {
|
|
149
|
+
const subject = () => {
|
|
150
|
+
const promise = loadScripts(bundleInfo);
|
|
151
|
+
onerrorRef!();
|
|
152
|
+
return promise;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
test('calls the original onerror handler', async () => {
|
|
156
|
+
await subject().catch(() => {});
|
|
157
|
+
|
|
158
|
+
expect(originalOnerrorSpy).toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('rejects with a network error', async () => {
|
|
162
|
+
await expect(subject()).rejects.toThrow(`Failed to load script: "${url}".`);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('removes the failed script tag from the DOM', async () => {
|
|
166
|
+
await subject().catch(() => {});
|
|
167
|
+
|
|
168
|
+
expect(scriptTag()).not.toBeInTheDocument();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createNetworkError } from '../common';
|
|
1
2
|
import { BundleInfo } from './get-bundle-info';
|
|
2
3
|
|
|
3
4
|
export function loadScripts({ urls, WebComponent, module }: BundleInfo) {
|
|
@@ -5,7 +6,7 @@ export function loadScripts({ urls, WebComponent, module }: BundleInfo) {
|
|
|
5
6
|
urls.js.map(url => {
|
|
6
7
|
const script = Array.from(document.scripts).find(({ src }) => src === url);
|
|
7
8
|
if (!script) {
|
|
8
|
-
return new Promise<void>(resolve => {
|
|
9
|
+
return new Promise<void>((resolve, reject) => {
|
|
9
10
|
const script = document.createElement('script');
|
|
10
11
|
|
|
11
12
|
if (module) {
|
|
@@ -18,8 +19,17 @@ export function loadScripts({ urls, WebComponent, module }: BundleInfo) {
|
|
|
18
19
|
script.onload = () => {
|
|
19
20
|
script.dataset.webComponent = WebComponent;
|
|
20
21
|
script.onload = null;
|
|
22
|
+
script.onerror = null;
|
|
21
23
|
resolve();
|
|
22
24
|
};
|
|
25
|
+
script.onerror = () => {
|
|
26
|
+
/*
|
|
27
|
+
* Remove the failed script so a later remount can retry
|
|
28
|
+
* instead of waiting on it forever.
|
|
29
|
+
*/
|
|
30
|
+
script.remove();
|
|
31
|
+
reject(createNetworkError(`Failed to load script: "${url}".`));
|
|
32
|
+
};
|
|
23
33
|
script.src = url;
|
|
24
34
|
|
|
25
35
|
document.body.append(script);
|
|
@@ -27,13 +37,20 @@ export function loadScripts({ urls, WebComponent, module }: BundleInfo) {
|
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
if (!script.dataset.webComponent) {
|
|
30
|
-
return new Promise<void>(resolve => {
|
|
40
|
+
return new Promise<void>((resolve, reject) => {
|
|
31
41
|
const originalOnload = script.onload;
|
|
42
|
+
const originalOnerror = script.onerror;
|
|
32
43
|
script.onload = (...args) => {
|
|
33
44
|
originalOnload?.call(script, ...args);
|
|
34
45
|
script.onload = null;
|
|
46
|
+
script.onerror = null;
|
|
35
47
|
resolve();
|
|
36
48
|
};
|
|
49
|
+
script.onerror = (...args) => {
|
|
50
|
+
originalOnerror?.call(script, ...args);
|
|
51
|
+
script.remove();
|
|
52
|
+
reject(createNetworkError(`Failed to load script: "${url}".`));
|
|
53
|
+
};
|
|
37
54
|
});
|
|
38
55
|
}
|
|
39
56
|
|