@servicetitan/web-components 31.0.0 → 31.2.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 (78) hide show
  1. package/dist/contexts/tests/mfe-data-context.test.js +34 -0
  2. package/dist/contexts/tests/mfe-data-context.test.js.map +1 -0
  3. package/dist/contexts/tests/mfe-metadata-context.test.js +41 -0
  4. package/dist/contexts/tests/mfe-metadata-context.test.js.map +1 -0
  5. package/dist/headless-loader.d.ts +5 -0
  6. package/dist/headless-loader.d.ts.map +1 -0
  7. package/dist/headless-loader.js +17 -0
  8. package/dist/headless-loader.js.map +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/loader.d.ts +10 -4
  14. package/dist/loader.d.ts.map +1 -1
  15. package/dist/loader.js +32 -40
  16. package/dist/loader.js.map +1 -1
  17. package/dist/register-headless.d.ts +9 -0
  18. package/dist/register-headless.d.ts.map +1 -0
  19. package/dist/register-headless.js +67 -0
  20. package/dist/register-headless.js.map +1 -0
  21. package/dist/types.d.ts +11 -0
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/types.js.map +1 -1
  24. package/dist/utils/get-bundle-info.d.ts +4 -12
  25. package/dist/utils/get-bundle-info.d.ts.map +1 -1
  26. package/dist/utils/get-bundle-info.js +29 -42
  27. package/dist/utils/get-bundle-info.js.map +1 -1
  28. package/dist/utils/get-cache-setting.d.ts +2 -0
  29. package/dist/utils/get-cache-setting.d.ts.map +1 -0
  30. package/dist/utils/get-cache-setting.js +6 -0
  31. package/dist/utils/get-cache-setting.js.map +1 -0
  32. package/dist/utils/get-cached-metadata.d.ts +18 -0
  33. package/dist/utils/get-cached-metadata.d.ts.map +1 -0
  34. package/dist/utils/get-cached-metadata.js +47 -0
  35. package/dist/utils/get-cached-metadata.js.map +1 -0
  36. package/dist/utils/get-mfe-has-compatible-launchdarkly-version.d.ts +1 -1
  37. package/dist/utils/get-mfe-has-compatible-launchdarkly-version.d.ts.map +1 -1
  38. package/dist/utils/get-mfe-has-compatible-launchdarkly-version.js.map +1 -1
  39. package/dist/utils/get-mfe-has-unique-chunk-namespace.d.ts +1 -1
  40. package/dist/utils/get-mfe-has-unique-chunk-namespace.d.ts.map +1 -1
  41. package/dist/utils/get-mfe-has-unique-chunk-namespace.js.map +1 -1
  42. package/dist/utils/get-mfe-supports-rerendering.d.ts +1 -1
  43. package/dist/utils/get-mfe-supports-rerendering.d.ts.map +1 -1
  44. package/dist/utils/get-mfe-supports-rerendering.js.map +1 -1
  45. package/dist/utils/get-version-mismatches.d.ts +1 -1
  46. package/dist/utils/get-version-mismatches.d.ts.map +1 -1
  47. package/dist/utils/get-version-mismatches.js +1 -1
  48. package/dist/utils/get-version-mismatches.js.map +1 -1
  49. package/dist/utils/web-component.d.ts +1 -1
  50. package/dist/utils/web-component.d.ts.map +1 -1
  51. package/dist/utils/web-component.js +12 -9
  52. package/dist/utils/web-component.js.map +1 -1
  53. package/package.json +12 -12
  54. package/src/__tests__/headless-loader.test.tsx +123 -0
  55. package/src/__tests__/loader.test.tsx +33 -11
  56. package/src/__tests__/register-headless.test.tsx +98 -0
  57. package/src/headless-loader.tsx +21 -0
  58. package/src/index.ts +2 -0
  59. package/src/loader.tsx +58 -46
  60. package/src/register-headless.ts +81 -0
  61. package/src/types.ts +9 -0
  62. package/src/utils/__tests__/get-version-mismatches.test.ts +1 -1
  63. package/src/utils/get-bundle-info.ts +37 -60
  64. package/src/utils/get-cache-setting.ts +5 -0
  65. package/src/utils/get-cached-metadata.ts +72 -0
  66. package/src/utils/get-mfe-has-compatible-launchdarkly-version.ts +1 -1
  67. package/src/utils/get-mfe-has-unique-chunk-namespace.ts +1 -1
  68. package/src/utils/get-mfe-supports-rerendering.ts +1 -1
  69. package/src/utils/get-version-mismatches.ts +1 -1
  70. package/src/utils/web-component.tsx +8 -6
  71. package/dist/__mocks__/expect-styles-in-dom.d.ts +0 -3
  72. package/dist/__mocks__/expect-styles-in-dom.d.ts.map +0 -1
  73. package/dist/__mocks__/index.d.ts +0 -4
  74. package/dist/__mocks__/index.d.ts.map +0 -1
  75. package/dist/__mocks__/register-component.d.ts +0 -4
  76. package/dist/__mocks__/register-component.d.ts.map +0 -1
  77. package/dist/__mocks__/test-component.d.ts +0 -3
  78. package/dist/__mocks__/test-component.d.ts.map +0 -1
@@ -2,28 +2,23 @@ import semver from 'semver';
2
2
  import get from 'lodash.get';
3
3
 
4
4
  import { ExposedDependencies, ExposedInstanceDependencies, getJson, GetJsonError } from '../common';
5
+ import { getCachedMetadata } from './get-cached-metadata';
6
+ import { getCacheSetting } from './get-cache-setting';
5
7
  import { EntryPoints, getEntrypoints } from './get-entry-points';
6
- import { getMFESupportsRerendering } from './get-mfe-supports-rerendering';
7
- import { getVersionMismatches } from './get-version-mismatches';
8
- import { isVersionedUrl } from './is-versioned-url';
9
8
  import { getMFEHasCompatibleLaunchDarklyVersion } from './get-mfe-has-compatible-launchdarkly-version';
9
+ import { getMFESupportsRerendering } from './get-mfe-supports-rerendering';
10
10
  import { getMFEHasUniqueChunkNamespace } from './get-mfe-has-unique-chunk-namespace';
11
-
12
- export interface Metadata {
13
- name: string;
14
- version?: string;
15
- bundledWith?: Record<string, string | undefined>;
16
- sharedDependencies: Record<string, string>;
17
- dependencies?: Record<string, string>;
18
- entrypoints?: Record<string, { css: string[]; js: string[] }>;
19
- }
11
+ import { getVersionMismatches } from './get-version-mismatches';
12
+ import type { Metadata } from '../types';
20
13
 
21
14
  export type BundleInfo = Awaited<ReturnType<typeof getBundleInfo>>;
22
15
 
23
16
  interface GetBundleInfoProps {
17
+ cache: boolean | number;
24
18
  exposedDependencies?: ExposedDependencies;
25
19
  exposedInstanceDependencies?: ExposedInstanceDependencies;
26
20
  fallbackPackageUrl?: string;
21
+ isHeadless?: boolean;
27
22
  mainPackageUrl: string;
28
23
  onRequestError?: (error: GetJsonError) => void;
29
24
  retries?: number;
@@ -31,15 +26,18 @@ interface GetBundleInfoProps {
31
26
  }
32
27
 
33
28
  export async function getBundleInfo({
29
+ cache,
34
30
  mainPackageUrl,
35
31
  fallbackPackageUrl,
36
32
  exposedDependencies = {},
37
33
  exposedInstanceDependencies = {},
34
+ isHeadless,
38
35
  signal,
39
36
  retries,
40
37
  onRequestError,
41
38
  }: GetBundleInfoProps) {
42
- const { data: metadata, url: packageUrl } = await getMetadata({
39
+ const { data: metadata, url: packageUrl } = await getCachedMetadata({
40
+ cache,
43
41
  fallbackPackageUrl,
44
42
  mainPackageUrl,
45
43
  onRequestError,
@@ -49,14 +47,14 @@ export async function getBundleInfo({
49
47
 
50
48
  const { bundledWith, name, sharedDependencies } = metadata;
51
49
  const exactPackageUrl = packageUrl.replace('/dist/metadata.json', '');
52
- const cache = getCacheSetting(exactPackageUrl);
50
+ const requestCache = getCacheSetting(exactPackageUrl);
53
51
 
54
52
  let dependencies: NonNullable<Metadata['dependencies']>;
55
53
  if (metadata.dependencies) {
56
54
  dependencies = metadata.dependencies;
57
55
  } else {
58
56
  const { data: packageJson } = await getJson(`${exactPackageUrl}/package.json`, {
59
- cache,
57
+ cache: requestCache,
60
58
  retries,
61
59
  signal,
62
60
  });
@@ -64,23 +62,31 @@ export async function getBundleInfo({
64
62
  dependencies = packageJson.dependencies;
65
63
  }
66
64
 
67
- const mismatch = getVersionMismatches(exposedDependencies, dependencies, sharedDependencies);
68
- const hasMismatch = !!Object.keys(mismatch).length;
69
- const startupVersion = bundledWith?.['@servicetitan/startup'];
70
- const supportLightBundles = startupVersion && semver.gte(startupVersion, '17.0.0');
71
- const embed = !hasMismatch && supportLightBundles;
72
- const bundleType = embed ? 'light' : 'full';
73
- const withBaseUrl = (path: string) => `${exactPackageUrl}/dist/bundle/${bundleType}/${path}`;
65
+ let mismatch: ReturnType<typeof getVersionMismatches>;
66
+ let resolvedBundleType: 'headless' | 'light' | 'full';
67
+
68
+ if (isHeadless) {
69
+ resolvedBundleType = 'headless';
70
+ } else {
71
+ mismatch = getVersionMismatches(exposedDependencies, dependencies, sharedDependencies);
72
+ const startupVersion = bundledWith?.['@servicetitan/startup'];
73
+ const supportLightBundles = !!startupVersion && semver.gte(startupVersion, '17.0.0');
74
+ const embed = !mismatch && supportLightBundles;
75
+ resolvedBundleType = embed ? 'light' : 'full';
76
+ }
77
+
78
+ const withBaseUrl = (path: string) =>
79
+ `${exactPackageUrl}/dist/bundle/${resolvedBundleType}/${path}`;
74
80
 
75
81
  let entrypoints: EntryPoints;
76
- if (metadata.entrypoints?.[bundleType]) {
77
- entrypoints = metadata.entrypoints[bundleType];
82
+ if (metadata.entrypoints?.[resolvedBundleType]) {
83
+ entrypoints = metadata.entrypoints[resolvedBundleType];
78
84
  } else {
79
- entrypoints = await getEntrypoints(withBaseUrl('entrypoints.json'), cache);
85
+ entrypoints = await getEntrypoints(withBaseUrl('entrypoints.json'), requestCache);
80
86
  }
81
87
 
82
88
  const disposer = () => {
83
- if (embed) {
89
+ if (resolvedBundleType === 'light') {
84
90
  const path = Object.values<string>(sharedDependencies).shift();
85
91
  if (path) {
86
92
  get(window, path).getStorage().clear(name);
@@ -93,6 +99,8 @@ export async function getBundleInfo({
93
99
  {} as EntryPoints
94
100
  );
95
101
 
102
+ const elementName = resolvedBundleType === 'headless' ? `headless-${name}` : name;
103
+
96
104
  return {
97
105
  bundledWith,
98
106
  disposer,
@@ -102,41 +110,10 @@ export async function getBundleInfo({
102
110
  bundledWith,
103
111
  exposedInstanceDependencies
104
112
  ),
105
- mismatch: hasMismatch ? mismatch : undefined,
113
+ mismatch,
114
+ resolvedBundleType,
106
115
  urls,
107
- WebComponent: name,
116
+ WebComponent: elementName,
108
117
  version: metadata.version,
109
118
  };
110
119
  }
111
-
112
- async function getMetadata(props: {
113
- fallbackPackageUrl?: string;
114
- mainPackageUrl: string;
115
- onRequestError?: (error: GetJsonError) => void;
116
- signal?: AbortSignal;
117
- retries?: number;
118
- }) {
119
- const { fallbackPackageUrl, mainPackageUrl, onRequestError, retries, signal } = props;
120
-
121
- return getJson<Metadata>(`${mainPackageUrl}/dist/metadata.json`, {
122
- cache: getCacheSetting(mainPackageUrl),
123
- retries: fallbackPackageUrl ? 0 : retries,
124
- onRequestError,
125
- signal,
126
- }).catch(e => {
127
- if (!fallbackPackageUrl) {
128
- throw e;
129
- }
130
-
131
- return getJson<Metadata>(`${fallbackPackageUrl}/dist/metadata.json`, {
132
- retries,
133
- onRequestError,
134
- cache: getCacheSetting(fallbackPackageUrl),
135
- signal,
136
- });
137
- });
138
- }
139
-
140
- function getCacheSetting(url: string) {
141
- return isVersionedUrl(url) ? undefined : 'no-store';
142
- }
@@ -0,0 +1,5 @@
1
+ import { isVersionedUrl } from './is-versioned-url';
2
+
3
+ export function getCacheSetting(url: string) {
4
+ return isVersionedUrl(url) ? undefined : 'no-store';
5
+ }
@@ -0,0 +1,72 @@
1
+ import { getJson, GetJsonError } from '../common';
2
+ import type { Metadata } from '../types';
3
+ import { getCacheSetting } from './get-cache-setting';
4
+ import { getMFEHasUniqueChunkNamespace } from './get-mfe-has-unique-chunk-namespace';
5
+ import { TtlCache } from './ttl-cache';
6
+
7
+ const MINUTE = 1000 * 60;
8
+
9
+ const metadataResponseCache = new TtlCache<Awaited<ReturnType<typeof getMetadata>>>({
10
+ ttlMs: 15 * MINUTE,
11
+ });
12
+
13
+ interface GetMetadataProps {
14
+ fallbackPackageUrl?: string;
15
+ mainPackageUrl: string;
16
+ onRequestError?: (error: GetJsonError) => void;
17
+ signal?: AbortSignal;
18
+ retries?: number;
19
+ }
20
+
21
+ async function getMetadata(props: GetMetadataProps) {
22
+ const { fallbackPackageUrl, mainPackageUrl, onRequestError, retries, signal } = props;
23
+
24
+ try {
25
+ return await getJson<Metadata>(`${mainPackageUrl}/dist/metadata.json`, {
26
+ cache: getCacheSetting(mainPackageUrl),
27
+ retries: fallbackPackageUrl ? 0 : retries,
28
+ onRequestError,
29
+ signal,
30
+ });
31
+ } catch (e) {
32
+ if (!fallbackPackageUrl) {
33
+ throw e;
34
+ }
35
+
36
+ return getJson<Metadata>(`${fallbackPackageUrl}/dist/metadata.json`, {
37
+ retries,
38
+ onRequestError,
39
+ cache: getCacheSetting(fallbackPackageUrl),
40
+ signal,
41
+ });
42
+ }
43
+ }
44
+
45
+ export async function getCachedMetadata(props: GetMetadataProps & { cache: boolean | number }) {
46
+ let cache = props.cache;
47
+ const cacheKey = props.mainPackageUrl;
48
+
49
+ let metadataResponse = cache ? metadataResponseCache.get(cacheKey) : undefined;
50
+
51
+ if (!metadataResponse) {
52
+ metadataResponse = await getMetadata(props);
53
+
54
+ const mfeHasUniqueChunkNamespace = getMFEHasUniqueChunkNamespace(
55
+ metadataResponse.data.bundledWith
56
+ );
57
+
58
+ if (cache === true && !mfeHasUniqueChunkNamespace) {
59
+ cache = -1;
60
+ }
61
+
62
+ if (cache) {
63
+ metadataResponseCache.set(
64
+ cacheKey,
65
+ metadataResponse,
66
+ typeof cache === 'number' ? cache : undefined
67
+ );
68
+ }
69
+ }
70
+
71
+ return metadataResponse;
72
+ }
@@ -1,5 +1,5 @@
1
1
  import { ExposedInstanceDependencies } from '../common';
2
- import { Metadata } from './get-bundle-info';
2
+ import type { Metadata } from '../types';
3
3
 
4
4
  /**
5
5
  * @param bundledWith THe MFE's metadata.bundledWith.
@@ -1,5 +1,5 @@
1
1
  import semver from 'semver';
2
- import { Metadata } from './get-bundle-info';
2
+ import type { Metadata } from '../types';
3
3
 
4
4
  // Check if the MFE loads it's chunks into a unique global chunk namespace
5
5
  export function getMFEHasUniqueChunkNamespace(bundledWith: Metadata['bundledWith']) {
@@ -1,5 +1,5 @@
1
1
  import semver from 'semver';
2
- import { Metadata } from './get-bundle-info';
2
+ import type { Metadata } from '../types';
3
3
 
4
4
  /*
5
5
  * Check if the MFE supports re-rendering itself when data changes
@@ -32,5 +32,5 @@ export const getVersionMismatches = (
32
32
  }
33
33
  }
34
34
 
35
- return mismatch;
35
+ return Object.keys(mismatch).length ? mismatch : undefined;
36
36
  };
@@ -23,7 +23,8 @@ interface WebComponentProps {
23
23
 
24
24
  export function webComponent(props: WebComponentProps) {
25
25
  const { basename, bundleInfo, eventBus, exposedDependencies, ldService, logService } = props;
26
- const { WebComponent, disposer, mfeSupportsRerendering, version } = bundleInfo;
26
+ const { WebComponent, resolvedBundleType, disposer, mfeSupportsRerendering, version } =
27
+ bundleInfo;
27
28
  const elementName = getElementName(bundleInfo);
28
29
 
29
30
  const exposedInstanceDependencies = ensureLDVersion(
@@ -31,7 +32,7 @@ export function webComponent(props: WebComponentProps) {
31
32
  bundleInfo.bundledWith?.['launchdarkly-js-client-sdk']
32
33
  );
33
34
 
34
- return ({ LoadingFallback, ...props }: LoaderDataProps) => {
35
+ return ({ LoadingFallback, ...innerProps }: LoaderDataProps) => {
35
36
  const [isLoading, setIsLoading] = useState(true);
36
37
  const [historyManager] = useOptionalDependencies(HistoryManager);
37
38
 
@@ -54,7 +55,7 @@ export function webComponent(props: WebComponentProps) {
54
55
  };
55
56
 
56
57
  // Stringify data object so that it can be passed as a data-attribute
57
- const stringifiedData = useMemo(() => JSON.stringify(props.data), [props.data]);
58
+ const stringifiedData = useMemo(() => JSON.stringify(innerProps.data), [innerProps.data]);
58
59
 
59
60
  /*
60
61
  * Set key for rendering the web component so that if the data from the Host change,
@@ -63,7 +64,7 @@ export function webComponent(props: WebComponentProps) {
63
64
  * This is only used as a fallback when the MFE's version is older.
64
65
  */
65
66
  const key = mfeSupportsRerendering ? undefined : stringifiedData;
66
- const dataAttrs = useMemo(() => composeDataAttributes(props.data), [props.data]);
67
+ const dataAttrs = useMemo(() => composeDataAttributes(innerProps.data), [innerProps.data]);
67
68
  const styleUrls = getStyleUrls(bundleInfo);
68
69
 
69
70
  return (
@@ -71,9 +72,8 @@ export function webComponent(props: WebComponentProps) {
71
72
  {isLoading && <LoadingFallback />}
72
73
  <WebComponent
73
74
  key={key}
74
- // @ts-expect-error because property ref does not exist on IntrinsicAttributes
75
75
  ref={handleRef}
76
- class={props.className}
76
+ class={innerProps.className}
77
77
  {...(styleUrls && { 'style-urls': styleUrls })}
78
78
  /*
79
79
  * Continue to spread data as separate data attributes for backwards compatibility,
@@ -83,6 +83,8 @@ export function webComponent(props: WebComponentProps) {
83
83
  data-mfe-data={stringifiedData}
84
84
  data-mfe-name={elementName}
85
85
  {...(version && { 'data-mfe-version': version })}
86
+ // @ts-expect-error style exists on HTMLElement
87
+ style={resolvedBundleType === 'headless' ? { display: 'none' } : undefined}
86
88
  />
87
89
  </Fragment>
88
90
  );
@@ -1,3 +0,0 @@
1
- import '@testing-library/jest-dom';
2
- export declare function expectStylesInDom(styleUrls: string | string[], shadowRootFn: () => ShadowRoot): void;
3
- //# sourceMappingURL=expect-styles-in-dom.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"expect-styles-in-dom.d.ts","sourceRoot":"","sources":["../../src/__mocks__/expect-styles-in-dom.ts"],"names":[],"mappings":"AAAA,OAAO,2BAA2B,CAAC;AAEnC,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,UAAU,QAQ7F"}
@@ -1,4 +0,0 @@
1
- export * from './expect-styles-in-dom';
2
- export * from './register-component';
3
- export * from './test-component';
4
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/__mocks__/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC"}
@@ -1,4 +0,0 @@
1
- import { FunctionComponent } from 'react';
2
- import { RegisterOptions } from '../register';
3
- export declare function registerComponent(TestComponent: FunctionComponent, componentName: string | undefined, options?: RegisterOptions): void;
4
- //# sourceMappingURL=register-component.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"register-component.d.ts","sourceRoot":"","sources":["../../src/__mocks__/register-component.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,wBAAgB,iBAAiB,CAC7B,aAAa,EAAE,iBAAiB,EAChC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,OAAO,CAAC,EAAE,eAAe,QAa5B"}
@@ -1,3 +0,0 @@
1
- import { FC } from 'react';
2
- export declare const MockComponent: FC<Record<string, any>>;
3
- //# sourceMappingURL=test-component.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"test-component.d.ts","sourceRoot":"","sources":["../../src/__mocks__/test-component.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAc,MAAM,OAAO,CAAC;AAMvC,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAqBjD,CAAC"}