@servicetitan/web-components 36.3.1 → 36.4.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.
Files changed (52) hide show
  1. package/dist/css-injector/index.d.ts +3 -0
  2. package/dist/css-injector/index.d.ts.map +1 -0
  3. package/dist/css-injector/index.js +4 -0
  4. package/dist/css-injector/index.js.map +1 -0
  5. package/dist/css-injector/mfe-style-registry.d.ts +14 -0
  6. package/dist/css-injector/mfe-style-registry.d.ts.map +1 -0
  7. package/dist/css-injector/mfe-style-registry.js +63 -0
  8. package/dist/css-injector/mfe-style-registry.js.map +1 -0
  9. package/dist/css-injector/with-css-injector.d.ts +6 -0
  10. package/dist/css-injector/with-css-injector.d.ts.map +1 -0
  11. package/dist/css-injector/with-css-injector.js +38 -0
  12. package/dist/css-injector/with-css-injector.js.map +1 -0
  13. package/dist/globals.d.ts +3 -3
  14. package/dist/globals.d.ts.map +1 -1
  15. package/dist/globals.js +19 -4
  16. package/dist/globals.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/register-headless.d.ts.map +1 -1
  22. package/dist/register-headless.js +1 -0
  23. package/dist/register-headless.js.map +1 -1
  24. package/dist/register.d.ts.map +1 -1
  25. package/dist/register.js +43 -37
  26. package/dist/register.js.map +1 -1
  27. package/dist/render.d.ts.map +1 -1
  28. package/dist/render.js +2 -2
  29. package/dist/render.js.map +1 -1
  30. package/dist/utils/get-bundle-info.d.ts.map +1 -1
  31. package/dist/utils/get-bundle-info.js +11 -4
  32. package/dist/utils/get-bundle-info.js.map +1 -1
  33. package/dist/utils/get-entry-points.d.ts +1 -0
  34. package/dist/utils/get-entry-points.d.ts.map +1 -1
  35. package/dist/utils/get-entry-points.js.map +1 -1
  36. package/package.json +23 -12
  37. package/src/__mocks__/register-anvil2-harness.tsx +49 -0
  38. package/src/__mocks__/test-component.tsx +9 -2
  39. package/src/__tests__/loader.test.tsx +32 -0
  40. package/src/__tests__/register-anvil2.test.tsx +21 -56
  41. package/src/css-injector/__tests__/mfe-style-registry.test.ts +163 -0
  42. package/src/css-injector/__tests__/with-css-injector.test.tsx +128 -0
  43. package/src/css-injector/index.ts +2 -0
  44. package/src/css-injector/mfe-style-registry.ts +48 -0
  45. package/src/css-injector/with-css-injector.tsx +40 -0
  46. package/src/globals.ts +30 -10
  47. package/src/index.ts +1 -0
  48. package/src/register-headless.ts +1 -0
  49. package/src/register.tsx +54 -43
  50. package/src/render.ts +2 -3
  51. package/src/utils/get-bundle-info.ts +12 -5
  52. package/src/utils/get-entry-points.ts +1 -0
@@ -43,12 +43,12 @@ export async function getBundleInfo({ cache, mainPackageUrl, fallbackPackageUrl,
43
43
  const embed = !mismatch && supportLightBundles;
44
44
  resolvedBundleType = embed ? 'light' : 'full';
45
45
  }
46
- const withBaseUrl = (path)=>`${exactPackageUrl}/dist/bundle/${resolvedBundleType}/${path}`;
46
+ const bundleBaseUrl = `${exactPackageUrl}/dist/bundle/${resolvedBundleType}`;
47
47
  let entrypoints;
48
48
  if ((_metadata_entrypoints = metadata.entrypoints) === null || _metadata_entrypoints === void 0 ? void 0 : _metadata_entrypoints[resolvedBundleType]) {
49
49
  entrypoints = metadata.entrypoints[resolvedBundleType];
50
50
  } else {
51
- entrypoints = await getEntrypoints(withBaseUrl('entrypoints.json'), requestCache);
51
+ entrypoints = await getEntrypoints(`${bundleBaseUrl}/entrypoints.json`, requestCache);
52
52
  }
53
53
  const disposer = ()=>{
54
54
  if (resolvedBundleType === 'light') {
@@ -58,9 +58,16 @@ export async function getBundleInfo({ cache, mainPackageUrl, fallbackPackageUrl,
58
58
  }
59
59
  }
60
60
  };
61
+ let baseUrl = bundleBaseUrl;
62
+ // Per-MFE dev-server port only happens during local development
63
+ if (process.env.NODE_ENV !== 'production' && entrypoints.port) {
64
+ const url = new URL(baseUrl);
65
+ url.port = String(entrypoints.port);
66
+ baseUrl = url.toString();
67
+ }
61
68
  const urls = {
62
- css: entrypoints.css.map(withBaseUrl),
63
- js: entrypoints.js.map(withBaseUrl)
69
+ css: entrypoints.css.map((path)=>`${baseUrl}/${path}`),
70
+ js: entrypoints.js.map((path)=>`${baseUrl}/${path}`)
64
71
  };
65
72
  const elementName = resolvedBundleType === 'headless' ? `headless-${name}` : name;
66
73
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/get-bundle-info.ts"],"sourcesContent":["import { ExposedDependencies, getVersionMismatches } from '@servicetitan/startup-utils';\nimport get from 'lodash.get';\nimport semver from 'semver';\nimport { ExposedInstanceDependencies, getJson, GetJsonError } from '../common';\nimport type { Metadata } from '../types';\nimport { getCacheSetting } from './get-cache-setting';\nimport { getCachedMetadata } from './get-cached-metadata';\nimport { EntryPoints, getEntrypoints } from './get-entry-points';\nimport { getMFEHasCompatibleLaunchDarklyVersion } from './get-mfe-has-compatible-launchdarkly-version';\nimport { getMFEHasUniqueChunkNamespace } from './get-mfe-has-unique-chunk-namespace';\nimport { getMFESupportsRerendering } from './get-mfe-supports-rerendering';\n\nexport type BundleInfo = Awaited<ReturnType<typeof getBundleInfo>>;\n\ninterface GetBundleInfoProps {\n cache: boolean | number;\n exposedDependencies?: ExposedDependencies;\n exposedInstanceDependencies?: ExposedInstanceDependencies;\n fallbackPackageUrl?: string;\n isHeadless?: boolean;\n mainPackageUrl: string;\n onRequestError?: (error: GetJsonError) => void;\n retries?: number;\n signal?: AbortSignal;\n}\n\nexport async function getBundleInfo({\n cache,\n mainPackageUrl,\n fallbackPackageUrl,\n exposedDependencies = {},\n exposedInstanceDependencies = {},\n isHeadless,\n signal,\n retries,\n onRequestError,\n}: GetBundleInfoProps) {\n const {\n cacheHit,\n data: metadata,\n url: packageUrl,\n } = await getCachedMetadata({\n cache,\n fallbackPackageUrl,\n mainPackageUrl,\n onRequestError,\n retries,\n signal,\n });\n\n const { bundledWith, name, sharedDependencies } = metadata;\n const exactPackageUrl = packageUrl.replace('/dist/metadata.json', '');\n const requestCache = getCacheSetting(exactPackageUrl);\n\n let dependencies: NonNullable<Metadata['dependencies']>;\n if (metadata.dependencies) {\n dependencies = metadata.dependencies;\n } else {\n const { data: packageJson } = await getJson(`${exactPackageUrl}/package.json`, {\n cache: requestCache,\n retries,\n signal,\n });\n\n dependencies = packageJson.dependencies;\n }\n\n let mismatch: ReturnType<typeof getVersionMismatches>;\n let resolvedBundleType: 'headless' | 'light' | 'full';\n\n if (isHeadless) {\n resolvedBundleType = 'headless';\n } else {\n mismatch = getVersionMismatches(exposedDependencies, dependencies, sharedDependencies);\n const startupVersion = bundledWith?.['@servicetitan/startup'];\n const supportLightBundles = !!startupVersion && semver.gte(startupVersion, '17.0.0');\n const embed = !mismatch && supportLightBundles;\n resolvedBundleType = embed ? 'light' : 'full';\n }\n\n const withBaseUrl = (path: string) =>\n `${exactPackageUrl}/dist/bundle/${resolvedBundleType}/${path}`;\n\n let entrypoints: EntryPoints;\n if (metadata.entrypoints?.[resolvedBundleType]) {\n entrypoints = metadata.entrypoints[resolvedBundleType];\n } else {\n entrypoints = await getEntrypoints(withBaseUrl('entrypoints.json'), requestCache);\n }\n\n const disposer = () => {\n if (resolvedBundleType === 'light') {\n const path = Object.values<string>(sharedDependencies).shift();\n if (path) {\n get(window, path).getStorage().clear(name);\n }\n }\n };\n\n const urls: EntryPoints = {\n css: entrypoints.css.map(withBaseUrl),\n js: entrypoints.js.map(withBaseUrl),\n };\n\n const elementName = resolvedBundleType === 'headless' ? `headless-${name}` : name;\n\n return {\n bundledWith,\n cacheHit,\n disposer,\n mfeSupportsRerendering: getMFESupportsRerendering(bundledWith, dependencies),\n mfeHasUniqueChunkNamespace: getMFEHasUniqueChunkNamespace(bundledWith),\n mfeHasCompatibleLaunchDarklyVersion: getMFEHasCompatibleLaunchDarklyVersion(\n bundledWith,\n exposedInstanceDependencies\n ),\n mismatch,\n resolvedBundleType,\n urls,\n WebComponent: elementName,\n version: metadata.version,\n module: entrypoints.module,\n };\n}\n"],"names":["getVersionMismatches","get","semver","getJson","getCacheSetting","getCachedMetadata","getEntrypoints","getMFEHasCompatibleLaunchDarklyVersion","getMFEHasUniqueChunkNamespace","getMFESupportsRerendering","getBundleInfo","cache","mainPackageUrl","fallbackPackageUrl","exposedDependencies","exposedInstanceDependencies","isHeadless","signal","retries","onRequestError","metadata","cacheHit","data","url","packageUrl","bundledWith","name","sharedDependencies","exactPackageUrl","replace","requestCache","dependencies","packageJson","mismatch","resolvedBundleType","startupVersion","supportLightBundles","gte","embed","withBaseUrl","path","entrypoints","disposer","Object","values","shift","window","getStorage","clear","urls","css","map","js","elementName","mfeSupportsRerendering","mfeHasUniqueChunkNamespace","mfeHasCompatibleLaunchDarklyVersion","WebComponent","version","module"],"mappings":"AAAA,SAA8BA,oBAAoB,QAAQ,8BAA8B;AACxF,OAAOC,SAAS,aAAa;AAC7B,OAAOC,YAAY,SAAS;AAC5B,SAAsCC,OAAO,QAAsB,YAAY;AAE/E,SAASC,eAAe,QAAQ,sBAAsB;AACtD,SAASC,iBAAiB,QAAQ,wBAAwB;AAC1D,SAAsBC,cAAc,QAAQ,qBAAqB;AACjE,SAASC,sCAAsC,QAAQ,gDAAgD;AACvG,SAASC,6BAA6B,QAAQ,uCAAuC;AACrF,SAASC,yBAAyB,QAAQ,iCAAiC;AAgB3E,OAAO,eAAeC,cAAc,EAChCC,KAAK,EACLC,cAAc,EACdC,kBAAkB,EAClBC,sBAAsB,CAAC,CAAC,EACxBC,8BAA8B,CAAC,CAAC,EAChCC,UAAU,EACVC,MAAM,EACNC,OAAO,EACPC,cAAc,EACG;QAgDbC;IA/CJ,MAAM,EACFC,QAAQ,EACRC,MAAMF,QAAQ,EACdG,KAAKC,UAAU,EAClB,GAAG,MAAMnB,kBAAkB;QACxBM;QACAE;QACAD;QACAO;QACAD;QACAD;IACJ;IAEA,MAAM,EAAEQ,WAAW,EAAEC,IAAI,EAAEC,kBAAkB,EAAE,GAAGP;IAClD,MAAMQ,kBAAkBJ,WAAWK,OAAO,CAAC,uBAAuB;IAClE,MAAMC,eAAe1B,gBAAgBwB;IAErC,IAAIG;IACJ,IAAIX,SAASW,YAAY,EAAE;QACvBA,eAAeX,SAASW,YAAY;IACxC,OAAO;QACH,MAAM,EAAET,MAAMU,WAAW,EAAE,GAAG,MAAM7B,QAAQ,GAAGyB,gBAAgB,aAAa,CAAC,EAAE;YAC3EjB,OAAOmB;YACPZ;YACAD;QACJ;QAEAc,eAAeC,YAAYD,YAAY;IAC3C;IAEA,IAAIE;IACJ,IAAIC;IAEJ,IAAIlB,YAAY;QACZkB,qBAAqB;IACzB,OAAO;QACHD,WAAWjC,qBAAqBc,qBAAqBiB,cAAcJ;QACnE,MAAMQ,iBAAiBV,wBAAAA,kCAAAA,WAAa,CAAC,wBAAwB;QAC7D,MAAMW,sBAAsB,CAAC,CAACD,kBAAkBjC,OAAOmC,GAAG,CAACF,gBAAgB;QAC3E,MAAMG,QAAQ,CAACL,YAAYG;QAC3BF,qBAAqBI,QAAQ,UAAU;IAC3C;IAEA,MAAMC,cAAc,CAACC,OACjB,GAAGZ,gBAAgB,aAAa,EAAEM,mBAAmB,CAAC,EAAEM,MAAM;IAElE,IAAIC;IACJ,KAAIrB,wBAAAA,SAASqB,WAAW,cAApBrB,4CAAAA,qBAAsB,CAACc,mBAAmB,EAAE;QAC5CO,cAAcrB,SAASqB,WAAW,CAACP,mBAAmB;IAC1D,OAAO;QACHO,cAAc,MAAMnC,eAAeiC,YAAY,qBAAqBT;IACxE;IAEA,MAAMY,WAAW;QACb,IAAIR,uBAAuB,SAAS;YAChC,MAAMM,OAAOG,OAAOC,MAAM,CAASjB,oBAAoBkB,KAAK;YAC5D,IAAIL,MAAM;gBACNvC,IAAI6C,QAAQN,MAAMO,UAAU,GAAGC,KAAK,CAACtB;YACzC;QACJ;IACJ;IAEA,MAAMuB,OAAoB;QACtBC,KAAKT,YAAYS,GAAG,CAACC,GAAG,CAACZ;QACzBa,IAAIX,YAAYW,EAAE,CAACD,GAAG,CAACZ;IAC3B;IAEA,MAAMc,cAAcnB,uBAAuB,aAAa,CAAC,SAAS,EAAER,MAAM,GAAGA;IAE7E,OAAO;QACHD;QACAJ;QACAqB;QACAY,wBAAwB7C,0BAA0BgB,aAAaM;QAC/DwB,4BAA4B/C,8BAA8BiB;QAC1D+B,qCAAqCjD,uCACjCkB,aACAV;QAEJkB;QACAC;QACAe;QACAQ,cAAcJ;QACdK,SAAStC,SAASsC,OAAO;QACzBC,QAAQlB,YAAYkB,MAAM;IAC9B;AACJ"}
1
+ {"version":3,"sources":["../../src/utils/get-bundle-info.ts"],"sourcesContent":["import { ExposedDependencies, getVersionMismatches } from '@servicetitan/startup-utils';\nimport get from 'lodash.get';\nimport semver from 'semver';\nimport { ExposedInstanceDependencies, getJson, GetJsonError } from '../common';\nimport type { Metadata } from '../types';\nimport { getCacheSetting } from './get-cache-setting';\nimport { getCachedMetadata } from './get-cached-metadata';\nimport { EntryPoints, getEntrypoints } from './get-entry-points';\nimport { getMFEHasCompatibleLaunchDarklyVersion } from './get-mfe-has-compatible-launchdarkly-version';\nimport { getMFEHasUniqueChunkNamespace } from './get-mfe-has-unique-chunk-namespace';\nimport { getMFESupportsRerendering } from './get-mfe-supports-rerendering';\n\nexport type BundleInfo = Awaited<ReturnType<typeof getBundleInfo>>;\n\ninterface GetBundleInfoProps {\n cache: boolean | number;\n exposedDependencies?: ExposedDependencies;\n exposedInstanceDependencies?: ExposedInstanceDependencies;\n fallbackPackageUrl?: string;\n isHeadless?: boolean;\n mainPackageUrl: string;\n onRequestError?: (error: GetJsonError) => void;\n retries?: number;\n signal?: AbortSignal;\n}\n\nexport async function getBundleInfo({\n cache,\n mainPackageUrl,\n fallbackPackageUrl,\n exposedDependencies = {},\n exposedInstanceDependencies = {},\n isHeadless,\n signal,\n retries,\n onRequestError,\n}: GetBundleInfoProps) {\n const {\n cacheHit,\n data: metadata,\n url: packageUrl,\n } = await getCachedMetadata({\n cache,\n fallbackPackageUrl,\n mainPackageUrl,\n onRequestError,\n retries,\n signal,\n });\n\n const { bundledWith, name, sharedDependencies } = metadata;\n const exactPackageUrl = packageUrl.replace('/dist/metadata.json', '');\n const requestCache = getCacheSetting(exactPackageUrl);\n\n let dependencies: NonNullable<Metadata['dependencies']>;\n if (metadata.dependencies) {\n dependencies = metadata.dependencies;\n } else {\n const { data: packageJson } = await getJson(`${exactPackageUrl}/package.json`, {\n cache: requestCache,\n retries,\n signal,\n });\n\n dependencies = packageJson.dependencies;\n }\n\n let mismatch: ReturnType<typeof getVersionMismatches>;\n let resolvedBundleType: 'headless' | 'light' | 'full';\n\n if (isHeadless) {\n resolvedBundleType = 'headless';\n } else {\n mismatch = getVersionMismatches(exposedDependencies, dependencies, sharedDependencies);\n const startupVersion = bundledWith?.['@servicetitan/startup'];\n const supportLightBundles = !!startupVersion && semver.gte(startupVersion, '17.0.0');\n const embed = !mismatch && supportLightBundles;\n resolvedBundleType = embed ? 'light' : 'full';\n }\n\n const bundleBaseUrl = `${exactPackageUrl}/dist/bundle/${resolvedBundleType}`;\n\n let entrypoints: EntryPoints;\n if (metadata.entrypoints?.[resolvedBundleType]) {\n entrypoints = metadata.entrypoints[resolvedBundleType];\n } else {\n entrypoints = await getEntrypoints(`${bundleBaseUrl}/entrypoints.json`, requestCache);\n }\n\n const disposer = () => {\n if (resolvedBundleType === 'light') {\n const path = Object.values<string>(sharedDependencies).shift();\n if (path) {\n get(window, path).getStorage().clear(name);\n }\n }\n };\n\n let baseUrl = bundleBaseUrl;\n // Per-MFE dev-server port only happens during local development\n if (process.env.NODE_ENV !== 'production' && entrypoints.port) {\n const url = new URL(baseUrl);\n url.port = String(entrypoints.port);\n baseUrl = url.toString();\n }\n\n const urls: EntryPoints = {\n css: entrypoints.css.map(path => `${baseUrl}/${path}`),\n js: entrypoints.js.map(path => `${baseUrl}/${path}`),\n };\n\n const elementName = resolvedBundleType === 'headless' ? `headless-${name}` : name;\n\n return {\n bundledWith,\n cacheHit,\n disposer,\n mfeSupportsRerendering: getMFESupportsRerendering(bundledWith, dependencies),\n mfeHasUniqueChunkNamespace: getMFEHasUniqueChunkNamespace(bundledWith),\n mfeHasCompatibleLaunchDarklyVersion: getMFEHasCompatibleLaunchDarklyVersion(\n bundledWith,\n exposedInstanceDependencies\n ),\n mismatch,\n resolvedBundleType,\n urls,\n WebComponent: elementName,\n version: metadata.version,\n module: entrypoints.module,\n };\n}\n"],"names":["getVersionMismatches","get","semver","getJson","getCacheSetting","getCachedMetadata","getEntrypoints","getMFEHasCompatibleLaunchDarklyVersion","getMFEHasUniqueChunkNamespace","getMFESupportsRerendering","getBundleInfo","cache","mainPackageUrl","fallbackPackageUrl","exposedDependencies","exposedInstanceDependencies","isHeadless","signal","retries","onRequestError","metadata","cacheHit","data","url","packageUrl","bundledWith","name","sharedDependencies","exactPackageUrl","replace","requestCache","dependencies","packageJson","mismatch","resolvedBundleType","startupVersion","supportLightBundles","gte","embed","bundleBaseUrl","entrypoints","disposer","path","Object","values","shift","window","getStorage","clear","baseUrl","process","env","NODE_ENV","port","URL","String","toString","urls","css","map","js","elementName","mfeSupportsRerendering","mfeHasUniqueChunkNamespace","mfeHasCompatibleLaunchDarklyVersion","WebComponent","version","module"],"mappings":"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"}
@@ -2,6 +2,7 @@ export interface EntryPoints {
2
2
  css: string[];
3
3
  js: string[];
4
4
  module?: true;
5
+ port?: number;
5
6
  }
6
7
  export declare function getEntrypoints(url: string, cache: 'no-store' | undefined, signal?: AbortSignal): Promise<EntryPoints>;
7
8
  //# sourceMappingURL=get-entry-points.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"get-entry-points.d.ts","sourceRoot":"","sources":["../../src/utils/get-entry-points.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IACxB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,MAAM,CAAC,EAAE,IAAI,CAAC;CACjB;AAED,wBAAsB,cAAc,CAChC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,MAAM,CAAC,EAAE,WAAW,GACrB,OAAO,CAAC,WAAW,CAAC,CAWtB"}
1
+ {"version":3,"file":"get-entry-points.d.ts","sourceRoot":"","sources":["../../src/utils/get-entry-points.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IACxB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,cAAc,CAChC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,MAAM,CAAC,EAAE,WAAW,GACrB,OAAO,CAAC,WAAW,CAAC,CAWtB"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/get-entry-points.ts"],"sourcesContent":["import { getJson } from '../common';\n\nexport interface EntryPoints {\n css: string[];\n js: string[];\n module?: true;\n}\n\nexport async function getEntrypoints(\n url: string,\n cache: 'no-store' | undefined,\n signal?: AbortSignal\n): Promise<EntryPoints> {\n try {\n return (\n await getJson(url, {\n cache,\n signal,\n })\n ).data;\n } catch {\n return { css: ['index.css'], js: ['index.js'] };\n }\n}\n"],"names":["getJson","getEntrypoints","url","cache","signal","data","css","js"],"mappings":"AAAA,SAASA,OAAO,QAAQ,YAAY;AAQpC,OAAO,eAAeC,eAClBC,GAAW,EACXC,KAA6B,EAC7BC,MAAoB;IAEpB,IAAI;QACA,OAAO,AACH,CAAA,MAAMJ,QAAQE,KAAK;YACfC;YACAC;QACJ,EAAC,EACHC,IAAI;IACV,EAAE,eAAM;QACJ,OAAO;YAAEC,KAAK;gBAAC;aAAY;YAAEC,IAAI;gBAAC;aAAW;QAAC;IAClD;AACJ"}
1
+ {"version":3,"sources":["../../src/utils/get-entry-points.ts"],"sourcesContent":["import { getJson } from '../common';\n\nexport interface EntryPoints {\n css: string[];\n js: string[];\n module?: true;\n port?: number;\n}\n\nexport async function getEntrypoints(\n url: string,\n cache: 'no-store' | undefined,\n signal?: AbortSignal\n): Promise<EntryPoints> {\n try {\n return (\n await getJson(url, {\n cache,\n signal,\n })\n ).data;\n } catch {\n return { css: ['index.css'], js: ['index.js'] };\n }\n}\n"],"names":["getJson","getEntrypoints","url","cache","signal","data","css","js"],"mappings":"AAAA,SAASA,OAAO,QAAQ,YAAY;AASpC,OAAO,eAAeC,eAClBC,GAAW,EACXC,KAA6B,EAC7BC,MAAoB;IAEpB,IAAI;QACA,OAAO,AACH,CAAA,MAAMJ,QAAQE,KAAK;YACfC;YACAC;QACJ,EAAC,EACHC,IAAI;IACV,EAAE,eAAM;QACJ,OAAO;YAAEC,KAAK;gBAAC;aAAY;YAAEC,IAAI;gBAAC;aAAW;QAAC;IAClD;AACJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/web-components",
3
- "version": "36.3.1",
3
+ "version": "36.4.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": {
@@ -10,24 +10,35 @@
10
10
  },
11
11
  "main": "./dist/index.js",
12
12
  "typings": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./register-headless": {
19
+ "types": "./dist/register-headless.d.ts",
20
+ "default": "./dist/register-headless.js"
21
+ },
22
+ "./*": "./dist/*.js"
23
+ },
13
24
  "sideEffects": false,
14
25
  "files": [
15
26
  "dist",
16
27
  "src"
17
28
  ],
18
29
  "dependencies": {
19
- "@servicetitan/error-boundary": "36.3.1",
20
- "@servicetitan/startup-utils": "36.3.1",
30
+ "@servicetitan/error-boundary": "36.4.0",
31
+ "@servicetitan/startup-utils": "36.4.0",
21
32
  "lodash.get": "~4.4.2",
22
33
  "mitt": "~3.0.1",
23
- "semver": "~7.8.0"
34
+ "semver": "~7.8.1"
24
35
  },
25
36
  "devDependencies": {
26
- "@servicetitan/anvil2": "^3.0.6",
37
+ "@servicetitan/anvil2": "^3.0.8",
27
38
  "@servicetitan/design-system": "~14.5.3",
28
- "@servicetitan/launchdarkly-service": "36.3.1",
29
- "@servicetitan/log-service": "36.3.1",
30
- "@servicetitan/react-ioc": "36.3.1",
39
+ "@servicetitan/launchdarkly-service": "36.4.0",
40
+ "@servicetitan/log-service": "36.4.0",
41
+ "@servicetitan/react-ioc": "36.4.0",
31
42
  "@testing-library/dom": "^10.4.1",
32
43
  "@testing-library/jest-dom": "^6.9.1",
33
44
  "@testing-library/react": "^16.3.2",
@@ -45,9 +56,9 @@
45
56
  "peerDependencies": {
46
57
  "@servicetitan/anvil2": ">=1.5.0",
47
58
  "@servicetitan/design-system": ">=14.5.3",
48
- "@servicetitan/launchdarkly-service": "36.3.1",
49
- "@servicetitan/log-service": "36.3.1",
50
- "@servicetitan/react-ioc": "36.3.1",
59
+ "@servicetitan/launchdarkly-service": "36.4.0",
60
+ "@servicetitan/log-service": "36.4.0",
61
+ "@servicetitan/react-ioc": "36.4.0",
51
62
  "history": "~4.10.1",
52
63
  "react": ">=17.0.2",
53
64
  "react-dom": ">=17.0.2"
@@ -69,5 +80,5 @@
69
80
  "cli": {
70
81
  "webpack": false
71
82
  },
72
- "gitHead": "93a049ba1e5cb737afb7bc95c06020736bc61a38"
83
+ "gitHead": "8beab9ae3a70ce381e8cd53f2de8691640e96c6e"
73
84
  }
@@ -0,0 +1,49 @@
1
+ import { render } from '@testing-library/react';
2
+ import { MockComponent, registerComponent } from './index';
3
+
4
+ /* Wrapper components injected by each optional library. */
5
+ export const optionalLibraryWrappers: Record<string, string[]> = {
6
+ '@servicetitan/anvil2': ['PortalProvider'],
7
+ '@servicetitan/design-system': [
8
+ 'DefaultPortalContext.Provider',
9
+ 'TablePopupPropsContext.Provider',
10
+ ],
11
+ };
12
+
13
+ export interface RegisterHarness {
14
+ Page: {
15
+ readonly componentShadowRoot: ShadowRoot;
16
+ readonly portalShadowRoot: ShadowRoot;
17
+ };
18
+ subject: () => ReturnType<typeof render>;
19
+ }
20
+
21
+ export function createRegisterHarness(
22
+ componentName: string,
23
+ options?: Parameters<typeof registerComponent>[2]
24
+ ): RegisterHarness {
25
+ const Page = {
26
+ get componentShadowRoot() {
27
+ return document.querySelector(componentName)!.shadowRoot!;
28
+ },
29
+ get portalShadowRoot() {
30
+ return document.querySelector('body')!.lastElementChild!.shadowRoot!;
31
+ },
32
+ };
33
+
34
+ beforeAll(() => {
35
+ registerComponent(MockComponent, componentName, {
36
+ sharedDependenciesNames: ['@servicetitan/anvil2'],
37
+ ...options,
38
+ });
39
+ });
40
+
41
+ const subject = () => {
42
+ jest.spyOn(console, 'error').mockImplementation(jest.fn()); // suppress "ReactDOM.render deprecated" error
43
+ const Component: any = componentName;
44
+ const handleRef = (wc: any) => wc?.provide({ foo: 'bar' }); // triggers rendering
45
+ return render(<Component ref={handleRef} />);
46
+ };
47
+
48
+ return { Page, subject };
49
+ }
@@ -1,13 +1,20 @@
1
1
  import { TablePopupPropsContext } from '@servicetitan/design-system';
2
- import { FC, useContext } from 'react';
2
+ import { createContext, FC, useContext } from 'react';
3
3
  import { useMFEDataContext, useMFEMetadataContext } from '../contexts';
4
4
 
5
5
  jest.mock('@progress/kendo-licensing', () => ({ validatePackage: jest.fn() }));
6
6
 
7
+ const FallbackContext = createContext(undefined);
8
+
7
9
  export const MockComponent: FC<Record<string, any>> = props => {
8
10
  const data = useMFEDataContext();
9
11
  const metadata = useMFEMetadataContext();
10
- const popupProps = useContext(TablePopupPropsContext);
12
+ /*
13
+ * TablePopupPropsContext is undefined when a test mocks
14
+ * @servicetitan/design-system as an empty object. Fall back to
15
+ * a no-op context so the component renders without it.
16
+ */
17
+ const popupProps = useContext(TablePopupPropsContext ?? FallbackContext);
11
18
  const appendTo = popupProps?.({}).appendTo;
12
19
 
13
20
  return (
@@ -403,6 +403,38 @@ describe('[web-components] Loader', () => {
403
403
  () => `${baseUrl}/dist/bundle/light/${metadata.entrypoints!.light.css[0]}`
404
404
  );
405
405
 
406
+ describe('when the entrypoint specifies a dev-server port', () => {
407
+ const port = 3000;
408
+
409
+ beforeEach(() => {
410
+ Object.assign(metadata.entrypoints!.light, { port });
411
+ });
412
+
413
+ function withPort(url: string) {
414
+ const result = new URL(url);
415
+ result.port = String(port);
416
+ return String(result);
417
+ }
418
+
419
+ itAddsJsEntrypointToWebComponent(
420
+ 'entrypoint with dev-server port',
421
+ () => `${withPort(baseUrl)}/dist/bundle/light/${metadata.entrypoints!.light.js[0]}`
422
+ );
423
+
424
+ describe('when NODE_ENV is production', () => {
425
+ const originalNodeEnv = process.env.NODE_ENV;
426
+
427
+ beforeEach(() => (process.env.NODE_ENV = 'production'));
428
+
429
+ afterEach(() => (process.env.NODE_ENV = originalNodeEnv));
430
+
431
+ itAddsJsEntrypointToWebComponent(
432
+ 'entrypoint without dev-server port',
433
+ () => `${baseUrl}/dist/bundle/light/${metadata.entrypoints!.light.js[0]}`
434
+ );
435
+ });
436
+ });
437
+
406
438
  describe('when entrypoint has module: true', () => {
407
439
  beforeEach(() => {
408
440
  metadata.entrypoints!.light.module = true;
@@ -1,7 +1,11 @@
1
1
  import '@testing-library/jest-dom';
2
- import { render, within } from '@testing-library/react';
2
+ import { within } from '@testing-library/react';
3
3
  import { Fragment } from 'react';
4
- import { expectStylesInDom, MockComponent, registerComponent } from '../__mocks__';
4
+ import { expectStylesInDom } from '../__mocks__';
5
+ import {
6
+ createRegisterHarness,
7
+ optionalLibraryWrappers,
8
+ } from '../__mocks__/register-anvil2-harness';
5
9
 
6
10
  function mockComponent(name: string) {
7
11
  return {
@@ -37,30 +41,7 @@ jest.mock('@servicetitan/design-system', () => ({
37
41
  }));
38
42
 
39
43
  describe('[web-components] register', () => {
40
- const componentName = 'mock-anvil2-app';
41
-
42
- class Page {
43
- static get componentShadowRoot() {
44
- return document.querySelector(componentName)!.shadowRoot!;
45
- }
46
-
47
- static get portalShadowRoot() {
48
- return document.querySelector('body')!.lastElementChild!.shadowRoot!;
49
- }
50
- }
51
-
52
- beforeAll(() => {
53
- registerComponent(MockComponent, componentName, {
54
- sharedDependenciesNames: ['@servicetitan/anvil2'],
55
- });
56
- });
57
-
58
- const subject = () => {
59
- jest.spyOn(console, 'error').mockImplementation(jest.fn()); // suppress "ReactDOM.render deprecated" error
60
- const Component: any = componentName;
61
- const handleRef = (webComponent: any) => webComponent?.provide({ foo: 'bar' }); // triggers rendering
62
- return render(<Component ref={handleRef} />);
63
- };
44
+ const { Page, subject } = createRegisterHarness('mock-anvil2-app');
64
45
 
65
46
  describe('when Anvil2 bundle is present in the DOM', () => {
66
47
  let cssLink: HTMLLinkElement;
@@ -90,44 +71,28 @@ describe('[web-components] register', () => {
90
71
  });
91
72
  });
92
73
 
93
- function itWrapsComponentWithin(name: string, expected: boolean = true) {
94
- test(`${expected ? 'wraps' : 'does not wrap'} component within ${name}`, () => {
74
+ for (const [library, wrappers] of Object.entries(optionalLibraryWrappers)) {
75
+ test.each(wrappers)(`wraps component within %s`, name => {
95
76
  subject();
96
77
 
97
78
  const element = within(Page.componentShadowRoot as any).queryByText(`<${name}>`);
98
- if (expected) {
99
- expect(element).toBeInTheDocument();
100
- } else {
101
- expect(element).not.toBeInTheDocument();
102
- }
79
+ expect(element).toBeInTheDocument();
103
80
  });
104
- }
105
-
106
- ['PortalProvider', 'DefaultPortalContext.Provider', 'TablePopupPropsContext.Provider'].forEach(
107
- name => itWrapsComponentWithin(name)
108
- );
109
81
 
110
- describe('when @servicetitan/anvil2 is not present', () => {
111
- beforeAll(() => {
112
- jest.resetModules();
113
- jest.mock('@servicetitan/anvil2', () => {
114
- throw new Error('Cannot find module');
82
+ describe(`when ${library} is not present`, () => {
83
+ beforeAll(() => {
84
+ jest.resetModules();
85
+ jest.mock(library, () => {
86
+ throw new Error('Cannot find module');
87
+ });
115
88
  });
116
- });
117
89
 
118
- itWrapsComponentWithin('PortalProvider', false);
119
- });
90
+ test.each(wrappers)('does not wrap component within %s', name => {
91
+ subject();
120
92
 
121
- describe('when @servicetitan/design-system is not present', () => {
122
- beforeAll(() => {
123
- jest.resetModules();
124
- jest.mock('@servicetitan/design-system', () => {
125
- throw new Error('Cannot find module');
93
+ const element = within(Page.componentShadowRoot as any).queryByText(`<${name}>`);
94
+ expect(element).not.toBeInTheDocument();
126
95
  });
127
96
  });
128
-
129
- ['DefaultPortalContext.Provider', 'TablePopupPropsContext.Provider'].forEach(name =>
130
- itWrapsComponentWithin(name, false)
131
- );
132
- });
97
+ }
133
98
  });
@@ -0,0 +1,163 @@
1
+ import { MfeStyleRegistry } from '../mfe-style-registry';
2
+
3
+ beforeAll(() => {
4
+ if (!CSSStyleSheet.prototype.replaceSync) {
5
+ CSSStyleSheet.prototype.replaceSync = jest.fn();
6
+ }
7
+ });
8
+
9
+ describe('[web-components] MfeStyleRegistry', () => {
10
+ const createRoot = () => {
11
+ const root = document.createElement('div').attachShadow({ mode: 'open' });
12
+ root.adoptedStyleSheets = [];
13
+ return root;
14
+ };
15
+
16
+ beforeEach(() => {
17
+ delete globalThis.__mfeStyles__;
18
+ });
19
+
20
+ describe(MfeStyleRegistry.for.name, () => {
21
+ test('initializes globalThis.__mfeStyles__', () => {
22
+ MfeStyleRegistry.for('test-mfe');
23
+
24
+ expect(globalThis.__mfeStyles__).toBeDefined();
25
+ });
26
+
27
+ test('returns the same registry for the same component', () => {
28
+ const a = MfeStyleRegistry.for('test-mfe');
29
+ const b = MfeStyleRegistry.for('test-mfe');
30
+
31
+ expect(a).toBe(b);
32
+ });
33
+
34
+ test('returns different registries for different components', () => {
35
+ const a = MfeStyleRegistry.for('mfe-a');
36
+ const b = MfeStyleRegistry.for('mfe-b');
37
+
38
+ expect(a).not.toBe(b);
39
+ });
40
+ });
41
+
42
+ describe('injectSheet', () => {
43
+ const defaultCss = '.foo { color: red; }';
44
+ const defaultSheetId = 'sheet-1';
45
+
46
+ let registry: MfeStyleRegistry;
47
+
48
+ beforeEach(() => {
49
+ registry = MfeStyleRegistry.for('test-mfe');
50
+ });
51
+
52
+ const subject = (sheetId = defaultSheetId, css = defaultCss) =>
53
+ registry.injectSheet(sheetId, css);
54
+
55
+ test('creates a new CSSStyleSheet and stores it', () => {
56
+ subject();
57
+
58
+ const sheet = registry.sheets.get('sheet-1');
59
+ expect(sheet).toBeInstanceOf(CSSStyleSheet);
60
+ expect(sheet!.replaceSync).toHaveBeenCalledWith(defaultCss);
61
+ });
62
+
63
+ test('adopts new sheets into observed roots', () => {
64
+ const root = createRoot();
65
+ registry.observeRoot(root);
66
+
67
+ subject();
68
+
69
+ expect(root.adoptedStyleSheets).toContain(registry.sheets.get('sheet-1'));
70
+ });
71
+
72
+ describe('when sheet with the same ID already exists', () => {
73
+ beforeEach(() => {
74
+ subject();
75
+ });
76
+
77
+ test('updates existing sheet in place via replaceSync', () => {
78
+ const sheet = registry.sheets.get(defaultSheetId)!;
79
+
80
+ const newCSS = '.foo { color: blue; }';
81
+ subject(defaultSheetId, newCSS);
82
+
83
+ expect(registry.sheets.get(defaultSheetId)).toBe(sheet);
84
+ expect(sheet.replaceSync).toHaveBeenLastCalledWith(newCSS);
85
+ });
86
+ });
87
+ });
88
+
89
+ describe('observeRoot', () => {
90
+ let registry: MfeStyleRegistry;
91
+
92
+ beforeEach(() => {
93
+ registry = MfeStyleRegistry.for('test-mfe');
94
+ });
95
+
96
+ test('adds root to the roots set', () => {
97
+ const root = createRoot();
98
+
99
+ registry.observeRoot(root);
100
+
101
+ expect(registry.roots.has(root)).toBe(true);
102
+ });
103
+
104
+ describe('when root is already observed', () => {
105
+ let root: ShadowRoot;
106
+
107
+ beforeEach(() => {
108
+ registry.injectSheet('a', '.a {}');
109
+ root = createRoot();
110
+ registry.observeRoot(root);
111
+ });
112
+
113
+ test('does not duplicate sheets', () => {
114
+ registry.observeRoot(root);
115
+
116
+ expect(root.adoptedStyleSheets).toHaveLength(1);
117
+ });
118
+ });
119
+
120
+ test('adopts all existing sheets into the root', () => {
121
+ registry.injectSheet('a', '.a {}');
122
+ registry.injectSheet('b', '.b {}');
123
+ const root = createRoot();
124
+
125
+ registry.observeRoot(root);
126
+
127
+ expect(root.adoptedStyleSheets).toEqual([...registry.sheets.values()]);
128
+ });
129
+ });
130
+
131
+ describe('unobserveRoot', () => {
132
+ let registry: MfeStyleRegistry;
133
+ let root: ShadowRoot;
134
+
135
+ beforeEach(() => {
136
+ registry = MfeStyleRegistry.for('test-mfe');
137
+ root = createRoot();
138
+ const defaultSheetId = 'sheet-1';
139
+ registry.injectSheet(defaultSheetId, '.foo {}');
140
+ registry.observeRoot(root);
141
+ });
142
+
143
+ test('removes root from the roots set', () => {
144
+ registry.unobserveRoot(root);
145
+
146
+ expect(registry.roots.has(root)).toBe(false);
147
+ });
148
+
149
+ test('removes adopted sheets from the root', () => {
150
+ registry.unobserveRoot(root);
151
+
152
+ expect(root.adoptedStyleSheets).toHaveLength(0);
153
+ });
154
+
155
+ test('stops receiving newly adopted sheets', () => {
156
+ registry.unobserveRoot(root);
157
+
158
+ registry.injectSheet('sheet-2', '.bar {}');
159
+
160
+ expect(root.adoptedStyleSheets).toHaveLength(0);
161
+ });
162
+ });
163
+ });