@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
@@ -0,0 +1,123 @@
1
+ import '@testing-library/jest-dom';
2
+ import { act, render, waitFor } from '@testing-library/react';
3
+ import * as common from '../common';
4
+ import { HeadlessLoader } from '../headless-loader';
5
+
6
+ describe('[web-components] HeadlessLoader', () => {
7
+ class Page {
8
+ static get webComponent() {
9
+ return document.querySelector(`headless-${WEB_COMPONENT_NAME}`);
10
+ }
11
+ }
12
+
13
+ class WebComponent extends HTMLElement {
14
+ provide = () => {};
15
+ }
16
+
17
+ const baseName = 'mfe-component';
18
+ const baseUrl = 'http://unpkg.servicetitan.com/mfe';
19
+ const WEB_COMPONENT_NAME = `${baseName}-abc123`;
20
+ const metadata = {
21
+ name: WEB_COMPONENT_NAME,
22
+ version: `0.0.0-test-abc123`,
23
+ sharedDependencies: {},
24
+ dependencies: {},
25
+ entrypoints: {
26
+ full: {
27
+ css: ['full.css'],
28
+ js: ['full.js'],
29
+ },
30
+ light: {
31
+ css: ['light.css'],
32
+ js: ['light.js'],
33
+ },
34
+ headless: {
35
+ js: ['headless.js'],
36
+ css: [],
37
+ },
38
+ },
39
+ };
40
+ const props = {
41
+ src: baseUrl,
42
+ loadingFallback: null,
43
+ };
44
+ let onloadRef: Function | null;
45
+ let getJsonMockFns: Record<string, Function>;
46
+
47
+ function initializeGetJsonMockFns() {
48
+ getJsonMockFns = {
49
+ [`${props.src}/dist/metadata.json`]: () => {
50
+ return Promise.resolve({
51
+ data: metadata,
52
+ headers: {} as Headers,
53
+ url: props.src,
54
+ });
55
+ },
56
+ };
57
+ }
58
+
59
+ beforeAll(() => {
60
+ // register web component in order to test the `provide` method
61
+ window.customElements.define(`headless-${WEB_COMPONENT_NAME}`, WebComponent);
62
+
63
+ /*
64
+ * mock <script>'s onload so that we can call it directly from the test,
65
+ * because it won't call itself in the test environment
66
+ */
67
+ Object.defineProperty(HTMLScriptElement.prototype, 'onload', {
68
+ get() {
69
+ return this._onload;
70
+ },
71
+ set(onload: Function) {
72
+ onloadRef = onload;
73
+ this._onload = onload;
74
+ },
75
+ });
76
+ });
77
+
78
+ beforeEach(() => {
79
+ jest.resetAllMocks();
80
+ onloadRef = null;
81
+ initializeGetJsonMockFns();
82
+ document.querySelectorAll('script').forEach(script => script.remove());
83
+ jest.spyOn(common, 'getJson').mockImplementation((url, _) => {
84
+ if (getJsonMockFns[url] === undefined) {
85
+ throw new Error(`Unexpected getJson call with url: ${url}`);
86
+ }
87
+ return getJsonMockFns[url]();
88
+ });
89
+ });
90
+
91
+ function subject() {
92
+ return render(<HeadlessLoader {...props} />);
93
+ }
94
+
95
+ describe('after the script tag added is loaded', () => {
96
+ async function scriptLoadedSetup() {
97
+ const result = subject();
98
+
99
+ await waitFor(() => onloadRef !== undefined);
100
+ act(() => {
101
+ onloadRef!();
102
+ });
103
+
104
+ await waitFor(() => expect(Page.webComponent).toBeInTheDocument());
105
+
106
+ return result;
107
+ }
108
+
109
+ test(`adds the headless entrypoint to a script tag`, async () => {
110
+ await scriptLoadedSetup();
111
+ const script = document.querySelector('script');
112
+ expect(script).toHaveAttribute(
113
+ 'src',
114
+ `${baseUrl}/dist/bundle/headless/${metadata.entrypoints.headless.js[0]}`
115
+ );
116
+ });
117
+
118
+ test('hides the web component', async () => {
119
+ await scriptLoadedSetup();
120
+ expect(Page.webComponent).toHaveStyle('display: none');
121
+ });
122
+ });
123
+ });
@@ -15,12 +15,14 @@ import {
15
15
  } from '../common';
16
16
  import { EVENT_BUS_TOKEN } from '../event-bus';
17
17
  import { HistoryManager } from '../history-manager';
18
- import { Loader, LoaderProps } from '../loader';
18
+ import { Loader, LoaderProps, UnrestrictedLoaderProps } from '../loader';
19
19
  import { Metadata } from '../utils/get-bundle-info';
20
20
  import { getMFEHasCompatibleLaunchDarklyVersion } from '../utils/get-mfe-has-compatible-launchdarkly-version';
21
+ import { getMFEHasUniqueChunkNamespace } from '../utils/get-mfe-has-unique-chunk-namespace';
21
22
  import { getVersionMismatches } from '../utils/get-version-mismatches';
22
23
 
23
24
  jest.mock('../utils/get-mfe-has-compatible-launchdarkly-version');
25
+ jest.mock('../utils/get-mfe-has-unique-chunk-namespace');
24
26
  jest.mock('../utils/get-version-mismatches');
25
27
 
26
28
  describe('[web-components] Loader', () => {
@@ -121,7 +123,8 @@ describe('[web-components] Loader', () => {
121
123
  return getJsonMockFns[url]();
122
124
  });
123
125
  jest.mocked(getMFEHasCompatibleLaunchDarklyVersion).mockReturnValue(true);
124
- jest.mocked(getVersionMismatches).mockReturnValue({});
126
+ jest.mocked(getVersionMismatches).mockReturnValue(undefined);
127
+ jest.mocked(getMFEHasUniqueChunkNamespace).mockReturnValue(true);
125
128
  });
126
129
 
127
130
  const Root = () => (
@@ -965,6 +968,12 @@ describe('[web-components] Loader', () => {
965
968
  describe('when component was previously loaded', () => {
966
969
  beforeEach(() => preloadComponent({ id: 'mfe1' }));
967
970
 
971
+ itNeverReloadsMetadata();
972
+ });
973
+
974
+ describe('when cache is true', () => {
975
+ beforeEach(() => preloadComponent({ id: 'mfe2', cache: true }));
976
+
968
977
  test('uses cached metadata', () => {
969
978
  subject();
970
979
 
@@ -974,22 +983,25 @@ describe('[web-components] Loader', () => {
974
983
  itReloadsMetadataAfter(1000 * 60 * 15, 'fifteen minutes');
975
984
  });
976
985
 
986
+ describe('when cache is true and mfe does not have unique chunk namespace', () => {
987
+ beforeEach(() => {
988
+ jest.mocked(getMFEHasUniqueChunkNamespace).mockReturnValue(false);
989
+ preloadComponent({ id: 'mfe3', cache: true });
990
+ });
991
+
992
+ itNeverReloadsMetadata();
993
+ });
994
+
977
995
  describe('when component has custom ttl', () => {
978
996
  const customTtl = 1000 * 60; // one minute
979
997
 
980
- beforeEach(() => preloadComponent({ id: 'mfe2', cache: customTtl }));
998
+ beforeEach(() => preloadComponent({ id: 'mfe4', cache: customTtl }));
981
999
 
982
1000
  itReloadsMetadataAfter(customTtl, 'custom ttl');
983
1001
  });
984
1002
 
985
- describe('when component has indefinite ttl', () => {
986
- beforeEach(() => preloadComponent({ id: 'mfe3', cache: -1 }));
987
-
988
- itNeverReloadsMetadata();
989
- });
990
-
991
1003
  describe('when component has zero ttl', () => {
992
- beforeEach(() => preloadComponent({ id: 'mfe4', cache: 0 }));
1004
+ beforeEach(() => preloadComponent({ id: 'mfe5', cache: 0 }));
993
1005
 
994
1006
  test('always reloads metadata', () => {
995
1007
  subject();
@@ -1002,11 +1014,21 @@ describe('[web-components] Loader', () => {
1002
1014
 
1003
1015
  describe("when mfe doesn't have unique chunk namespace", () => {
1004
1016
  beforeEach(() => {
1005
- preloadComponent({ id: 'mfe5' });
1017
+ preloadComponent({ id: 'mfe6' });
1006
1018
  setStartupVersion('27.1.1');
1007
1019
  });
1008
1020
 
1009
1021
  itNeverReloadsMetadata();
1010
1022
  });
1023
+
1024
+ describe('when headless mfe is loaded after non-headless mfe', () => {
1025
+ beforeEach(() => {
1026
+ preloadComponent({ id: 'mfe7' });
1027
+ (props as UnrestrictedLoaderProps).isHeadless = true;
1028
+ initializeGetJsonMockFns(props.src);
1029
+ });
1030
+
1031
+ itNeverReloadsMetadata();
1032
+ });
1011
1033
  });
1012
1034
  });
@@ -0,0 +1,98 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, waitFor } from '@testing-library/react';
3
+
4
+ import { Entries, IWebComponent } from '../common';
5
+ import { HeadlessCallback } from '../register-headless';
6
+
7
+ export function registerComponent(
8
+ connectedCallback: HeadlessCallback<unknown>,
9
+ disconnectedCallback: HeadlessCallback<unknown>,
10
+ componentName: string | undefined
11
+ ) {
12
+ /*
13
+ * This register can only happen once, unfortunately. So the connectedCallback and
14
+ * disconnectedCallback need to work for all tests. If run more than once,
15
+ * window.customElements.define will throw the error:
16
+ * "NotSupportedError: This name has already been registered in the registry."
17
+ *
18
+ * Also note that there is no way to "unregister" a web component, so an afterAll
19
+ * cannot be used.
20
+ */
21
+ (global as any).WEB_COMPONENT_NAME = componentName;
22
+ const { registerHeadless } = require('../register-headless');
23
+ registerHeadless(connectedCallback, disconnectedCallback);
24
+ }
25
+
26
+ describe('[web-components] registerHeadless', () => {
27
+ const baseComponentName = 'mock-app';
28
+ const componentName = `${baseComponentName}-40b38a39`;
29
+ const connectedCallback = jest.fn().mockName('connectedCallback');
30
+ const disconnectedCallback = jest.fn().mockName('disconnectedCallback');
31
+ const provideProps = {
32
+ ldService: 'ld-service',
33
+ logService: 'log-service',
34
+ eventBus: 'event-bus',
35
+ } as unknown as Entries;
36
+ const mfeData = { foo: 'bar' };
37
+ const handleRef = (webComponent: IWebComponent | null) => {
38
+ webComponent?.provide(provideProps);
39
+ };
40
+
41
+ beforeAll(() => {
42
+ registerComponent(connectedCallback, disconnectedCallback, componentName);
43
+ });
44
+
45
+ const subject = (props: Record<string, any> = {}, dataAttrs: Record<string, any> = {}) => {
46
+ jest.spyOn(console, 'error').mockImplementation(jest.fn()); // suppress "attempt to unmount root while rendering" error
47
+
48
+ const data = props && { 'data-mfe-data': JSON.stringify(props) };
49
+ const Component: any = `headless-${componentName}`;
50
+ const renderResult = render(<Component ref={handleRef} {...data} {...dataAttrs} />);
51
+
52
+ return renderResult;
53
+ };
54
+
55
+ beforeEach(() => {
56
+ jest.clearAllMocks();
57
+ });
58
+
59
+ [connectedCallback, disconnectedCallback].forEach(callback => {
60
+ test(`calls ${callback.getMockName()} with mfeData`, async () => {
61
+ const renderResult = subject({}, { 'data-mfe-data': JSON.stringify(mfeData) });
62
+ renderResult.unmount();
63
+
64
+ await waitFor(() => {
65
+ expect(callback).toHaveBeenCalledWith(
66
+ expect.objectContaining({
67
+ mfeData,
68
+ })
69
+ );
70
+ });
71
+ });
72
+ });
73
+
74
+ [connectedCallback, disconnectedCallback].forEach(callback => {
75
+ (['ldService', 'logService', 'eventBus'] as const).forEach(service => {
76
+ test(`calls ${callback.getMockName()} with ${service}`, async () => {
77
+ const renderResult = subject();
78
+ renderResult.unmount();
79
+
80
+ await waitFor(() => {
81
+ expect(callback).toHaveBeenCalledWith(
82
+ expect.objectContaining({
83
+ [service]: provideProps[service],
84
+ })
85
+ );
86
+ });
87
+ });
88
+ });
89
+ });
90
+
91
+ test('logs an error when WEB_COMPONENT_NAME is not set', () => {
92
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn());
93
+ jest.isolateModules(() => {
94
+ registerComponent(connectedCallback, disconnectedCallback, undefined);
95
+ });
96
+ expect(consoleSpy).toHaveBeenCalledWith('"WEB_COMPONENT_NAME" is not defined!');
97
+ });
98
+ });
@@ -0,0 +1,21 @@
1
+ import { UnrestrictedLoaderProps, Loader } from './loader';
2
+
3
+ type HeadlessLoaderProps<T> = Pick<
4
+ UnrestrictedLoaderProps<T>,
5
+ 'src' | 'fallbackSrc' | 'data' | 'className' | 'retries' | 'cache' | 'onRequestError'
6
+ >;
7
+
8
+ const UnrestrictedPropsLoader = <T,>(props: UnrestrictedLoaderProps<T>) => {
9
+ return <Loader {...props} />;
10
+ };
11
+
12
+ export function HeadlessLoader<T>(props: Readonly<HeadlessLoaderProps<T>>) {
13
+ return (
14
+ <UnrestrictedPropsLoader<T>
15
+ {...props}
16
+ isHeadless
17
+ loadingFallback={null}
18
+ errorFallback={null}
19
+ />
20
+ );
21
+ }
package/src/index.ts CHANGED
@@ -2,8 +2,10 @@ export * from './common';
2
2
  export * from './contexts';
3
3
  export * from './event-bus';
4
4
  export * from './get-value-for-environment';
5
+ export * from './headless-loader';
5
6
  export * from './history-manager';
6
7
  export * from './loader';
7
8
  export * from './register';
9
+ export * from './register-headless';
8
10
 
9
11
  export type { JSON, JSONPrimitive, Serializable } from './types';
package/src/loader.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ReactElement,
3
3
  ReactNode,
4
+ Ref,
4
5
  Suspense,
5
6
  forwardRef,
6
7
  lazy,
@@ -28,7 +29,6 @@ import { EventBus, EVENT_BUS_TOKEN } from './event-bus';
28
29
  import { EXPOSED_DEPENDENCIES, EXPOSED_INSTANCE_DEPENDENCIES } from './globals';
29
30
  import {
30
31
  BundleInfo,
31
- TtlCache,
32
32
  getBundleInfo,
33
33
  getLDServiceToken,
34
34
  validateBundleInfo,
@@ -40,7 +40,7 @@ export type LoaderRef = EventBus;
40
40
 
41
41
  export type FallbackNode = NonNullable<ReactNode> | null;
42
42
 
43
- export interface LoaderProps<T> {
43
+ export interface UnrestrictedLoaderProps<T = {}> {
44
44
  src: string;
45
45
  fallbackSrc?: string;
46
46
  /** Values in the object will be stringified and then parsed before being delivered to the MFE via props or context. */
@@ -51,16 +51,32 @@ export interface LoaderProps<T> {
51
51
  errorFallback?: FallbackNode;
52
52
  className?: string;
53
53
  retries?: number;
54
+ isHeadless?: boolean;
54
55
  cache?: boolean | number;
55
56
  onRequestError?(error: GetJsonError): boolean;
56
57
  }
57
58
 
58
- export type LoaderDataProps = Pick<LoaderProps<{}>, 'data' | 'className'> & {
59
+ export type LoaderProps<T> = Pick<
60
+ UnrestrictedLoaderProps<T>,
61
+ | 'src'
62
+ | 'fallbackSrc'
63
+ | 'data'
64
+ | 'basename'
65
+ | 'loadingFallback'
66
+ | 'loadingFallbackDelayed'
67
+ | 'errorFallback'
68
+ | 'className'
69
+ | 'retries'
70
+ | 'cache'
71
+ | 'onRequestError'
72
+ >;
73
+
74
+ export type LoaderDataProps = Pick<UnrestrictedLoaderProps, 'data' | 'className'> & {
59
75
  LoadingFallback: () => JSX.Element;
60
76
  };
61
77
 
62
- export const Loader = forwardRef<LoaderRef, LoaderProps<{}>>((props, ref) => {
63
- const { cache = true, basename, fallbackSrc, onRequestError, retries, src } = props;
78
+ const UnrestrictedLoader = <T,>(props: UnrestrictedLoaderProps<T>, ref: Ref<LoaderRef>) => {
79
+ const { cache = -1, basename, fallbackSrc, isHeadless, onRequestError, retries, src } = props;
64
80
  const { loadingFallback, loadingFallbackDelayed = true } = props;
65
81
 
66
82
  const bus = useEventBus();
@@ -97,6 +113,7 @@ export const Loader = forwardRef<LoaderRef, LoaderProps<{}>>((props, ref) => {
97
113
  exposedInstanceDependencies,
98
114
  eventBus,
99
115
  fallbackSrc,
116
+ isHeadless,
100
117
  logService,
101
118
  onRequestError,
102
119
  retries,
@@ -109,9 +126,10 @@ export const Loader = forwardRef<LoaderRef, LoaderProps<{}>>((props, ref) => {
109
126
  exposedDependencies,
110
127
  exposedInstanceDependencies,
111
128
  eventBus,
129
+ fallbackSrc,
130
+ isHeadless,
112
131
  logService,
113
132
  onRequestError,
114
- fallbackSrc,
115
133
  retries,
116
134
  src,
117
135
  ]
@@ -130,14 +148,19 @@ export const Loader = forwardRef<LoaderRef, LoaderProps<{}>>((props, ref) => {
130
148
  </Suspense>
131
149
  </ErrorBoundary>
132
150
  );
133
- });
151
+ };
134
152
 
135
- type LazyLoaderProps = LoaderProps<any> & {
153
+ export const Loader = forwardRef(UnrestrictedLoader) as <T = {}>(
154
+ props: LoaderProps<NoInfer<T>> & { ref?: Ref<LoaderRef> }
155
+ ) => ReturnType<typeof UnrestrictedLoader>;
156
+
157
+ type LazyLoaderProps = UnrestrictedLoaderProps<any> & {
136
158
  cancelation: AbortController;
137
159
  exposedDependencies?: ExposedDependencies;
138
160
  exposedInstanceDependencies?: ExposedInstanceDependencies;
139
161
  eventBus: EventBus;
140
162
  logService?: Log;
163
+ cache: Required<UnrestrictedLoaderProps<any>>['cache'];
141
164
  };
142
165
 
143
166
  function lazyLoader(props: LazyLoaderProps) {
@@ -165,58 +188,50 @@ function lazyLoader(props: LazyLoaderProps) {
165
188
  function lazyWebComponent(props: LazyLoaderProps) {
166
189
  const {
167
190
  basename,
191
+ cache,
168
192
  cancelation,
169
193
  exposedDependencies,
170
194
  exposedInstanceDependencies,
171
195
  eventBus,
172
196
  fallbackSrc,
197
+ isHeadless,
173
198
  logService,
174
199
  onRequestError,
175
200
  retries,
176
201
  src,
177
202
  } = props;
178
203
 
179
- let { cache } = props;
180
-
181
204
  return lazy<(_: LoaderDataProps) => JSX.Element | null>(async () => {
182
- let bundleInfo: BundleInfo | undefined = cache ? bundleCache.get(src) : undefined;
183
- if (!bundleInfo) {
184
- try {
185
- bundleInfo = await getBundleInfo({
186
- mainPackageUrl: src,
187
- fallbackPackageUrl: fallbackSrc,
188
- exposedDependencies,
189
- exposedInstanceDependencies,
190
- signal: cancelation.signal,
191
- retries,
192
- onRequestError,
193
- });
194
- } catch (e) {
195
- if (cancelation.signal.aborted) {
196
- return {
197
- default: () => null,
198
- };
199
- }
200
-
201
- throw e;
202
- }
203
-
204
- if (cache === true && !bundleInfo.mfeHasUniqueChunkNamespace) {
205
- cache = -1;
206
- }
207
-
208
- if (cache) {
209
- bundleCache.set(src, bundleInfo, typeof cache === 'number' ? cache : undefined);
210
- }
211
-
212
- validateBundleInfo(bundleInfo, {
205
+ let bundleInfo: BundleInfo;
206
+ try {
207
+ bundleInfo = await getBundleInfo({
208
+ cache,
209
+ mainPackageUrl: src,
210
+ fallbackPackageUrl: fallbackSrc,
211
+ isHeadless,
213
212
  exposedDependencies,
214
213
  exposedInstanceDependencies,
215
- logService,
216
- packageUrl: src,
214
+ signal: cancelation.signal,
215
+ retries,
216
+ onRequestError,
217
217
  });
218
+ } catch (e) {
219
+ if (cancelation.signal.aborted) {
220
+ return {
221
+ default: () => null,
222
+ };
223
+ }
224
+
225
+ throw e;
218
226
  }
219
227
 
228
+ validateBundleInfo(bundleInfo, {
229
+ exposedDependencies,
230
+ exposedInstanceDependencies,
231
+ logService,
232
+ packageUrl: src,
233
+ });
234
+
220
235
  await loadScripts(bundleInfo);
221
236
 
222
237
  const ldService = bundleInfo.mfeHasCompatibleLaunchDarklyVersion
@@ -264,6 +279,3 @@ function ensureLDService(
264
279
  function useEventBus() {
265
280
  return useRef(new EventBus()).current;
266
281
  }
267
-
268
- const MINUTE = 1000 * 60;
269
- const bundleCache = new TtlCache<BundleInfo>({ ttlMs: 15 * MINUTE });
@@ -0,0 +1,81 @@
1
+ import type { LDService } from '@servicetitan/launchdarkly-service';
2
+ import type { Entries, IWebComponent } from './common';
3
+ import type { EventBus } from './event-bus';
4
+ import type { Log } from '@servicetitan/log-service';
5
+ import { Serializable } from './types';
6
+
7
+ type HeadlessCallbackArgs<T> = Pick<Entries, 'ldService' | 'logService' | 'eventBus'> & {
8
+ mfeData?: Serializable<T>;
9
+ };
10
+ export type HeadlessCallback<T = undefined> = (params: HeadlessCallbackArgs<T>) => void;
11
+
12
+ export function registerHeadless(
13
+ connectedCallback?: HeadlessCallback<unknown>,
14
+ disconnectedCallback?: HeadlessCallback<unknown>
15
+ ) {
16
+ class Headless extends HTMLElement implements IWebComponent {
17
+ private ldService?: LDService;
18
+ private logService?: Log;
19
+ private eventBus?: EventBus;
20
+ private readonly readyPromise: Promise<void>;
21
+ private resolveReadyPromise!: () => void;
22
+
23
+ constructor() {
24
+ super();
25
+ this.readyPromise = new Promise(resolve => {
26
+ this.resolveReadyPromise = resolve;
27
+ });
28
+ }
29
+
30
+ provide = (entries: Entries) => {
31
+ this.ldService = entries.ldService;
32
+ this.logService = entries.logService;
33
+ this.eventBus = entries.eventBus;
34
+ this.resolveReadyPromise();
35
+ };
36
+
37
+ async connectedCallback() {
38
+ await this.readyPromise;
39
+ connectedCallback?.({
40
+ ...this.services,
41
+ mfeData: this.mfeData,
42
+ });
43
+ }
44
+
45
+ async disconnectedCallback() {
46
+ await this.readyPromise;
47
+ disconnectedCallback?.({
48
+ ...this.services,
49
+ mfeData: this.mfeData,
50
+ });
51
+ }
52
+
53
+ connectedMoveCallback() {
54
+ /*
55
+ * Defining connectedMoveCallback prevents connectedCallback or disconnectedCallback
56
+ * from running when the element is moved in the DOM
57
+ * https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks
58
+ */
59
+ }
60
+
61
+ private get services() {
62
+ return {
63
+ ldService: this.ldService,
64
+ logService: this.logService,
65
+ eventBus: this.eventBus,
66
+ };
67
+ }
68
+
69
+ private get mfeData() {
70
+ return JSON.parse(this.dataset.mfeData ?? '{}');
71
+ }
72
+ }
73
+
74
+ if (!WEB_COMPONENT_NAME) {
75
+ // eslint-disable-next-line no-console
76
+ console.error('"WEB_COMPONENT_NAME" is not defined!');
77
+ return;
78
+ }
79
+
80
+ window.customElements.define(`headless-${WEB_COMPONENT_NAME}`, Headless);
81
+ }
package/src/types.ts CHANGED
@@ -11,3 +11,12 @@ export type Serializable<T> = {
11
11
  ? Serializable<U>[]
12
12
  : Serializable<T[P]>;
13
13
  };
14
+
15
+ export interface Metadata {
16
+ name: string;
17
+ version?: string;
18
+ bundledWith?: Record<string, string | undefined>;
19
+ sharedDependencies: Record<string, string>;
20
+ dependencies?: Record<string, string>;
21
+ entrypoints?: Record<string, { css: string[]; js: string[] }>;
22
+ }
@@ -27,7 +27,7 @@ describe(`[web-components] ${getVersionMismatches.name}`, () => {
27
27
  test('returns no mismatches', () => {
28
28
  expect(
29
29
  getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
30
- ).toEqual({});
30
+ ).toEqual(undefined);
31
31
  });
32
32
  }
33
33