@servicetitan/web-components 39.2.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 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;
@@ -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;AAUD,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"}
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;
@@ -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\nfunction 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,SAASM,mBAAmBC,OAAe;IACvC,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
+ {"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":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,UAAU,mBAwCrE"}
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":"AAEA,OAAO,SAASA,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,QAAcY,CAAAA;gBACrB,MAAMP,SAASG,SAASK,aAAa,CAAC;gBAEtC,IAAId,QAAQ;oBACRM,OAAOS,IAAI,GAAG;gBAClB,OAAO;oBACHT,OAAOU,KAAK,GAAG;gBACnB;gBAEAV,OAAOW,WAAW,GAAG;gBACrBX,OAAOY,MAAM,GAAG;oBACZZ,OAAOa,OAAO,CAACC,YAAY,GAAGrB;oBAC9BO,OAAOY,MAAM,GAAG;oBAChBL;gBACJ;gBACAP,OAAOM,GAAG,GAAGP;gBAEbI,SAASY,IAAI,CAACC,MAAM,CAAChB;YACzB;QACJ;QAEA,IAAI,CAACA,OAAOa,OAAO,CAACC,YAAY,EAAE;YAC9B,OAAO,IAAInB,QAAcY,CAAAA;gBACrB,MAAMU,iBAAiBjB,OAAOY,MAAM;gBACpCZ,OAAOY,MAAM,GAAG,CAAC,GAAGM;oBAChBD,2BAAAA,qCAAAA,eAAgBE,IAAI,CAACnB,WAAWkB;oBAChClB,OAAOY,MAAM,GAAG;oBAChBL;gBACJ;YACJ;QACJ;QAEA,OAAOZ,QAAQY,OAAO;IAC1B;AAER"}
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.2.0",
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,18 +31,18 @@
31
31
  "src"
32
32
  ],
33
33
  "dependencies": {
34
- "@servicetitan/error-boundary": "39.2.0",
35
- "@servicetitan/startup-utils": "39.2.0",
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.13.2",
41
+ "@servicetitan/anvil2": "^3.13.3",
42
42
  "@servicetitan/design-system": "~14.5.3",
43
- "@servicetitan/launchdarkly-service": "39.2.0",
44
- "@servicetitan/log-service": "39.2.0",
45
- "@servicetitan/react-ioc": "39.2.0",
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
48
  "@testing-library/react": "^16.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.2.0",
64
- "@servicetitan/log-service": "39.2.0",
65
- "@servicetitan/react-ioc": "39.2.0",
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"
@@ -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 it directly from the test,
66
- * because it won't call itself in the test environment
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
- test('renders default error fallback', async () => {
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