@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.
- package/dist/contexts/tests/mfe-data-context.test.js +34 -0
- package/dist/contexts/tests/mfe-data-context.test.js.map +1 -0
- package/dist/contexts/tests/mfe-metadata-context.test.js +41 -0
- package/dist/contexts/tests/mfe-metadata-context.test.js.map +1 -0
- package/dist/headless-loader.d.ts +5 -0
- package/dist/headless-loader.d.ts.map +1 -0
- package/dist/headless-loader.js +17 -0
- package/dist/headless-loader.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +10 -4
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +32 -40
- package/dist/loader.js.map +1 -1
- package/dist/register-headless.d.ts +9 -0
- package/dist/register-headless.d.ts.map +1 -0
- package/dist/register-headless.js +67 -0
- package/dist/register-headless.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/get-bundle-info.d.ts +4 -12
- package/dist/utils/get-bundle-info.d.ts.map +1 -1
- package/dist/utils/get-bundle-info.js +29 -42
- package/dist/utils/get-bundle-info.js.map +1 -1
- package/dist/utils/get-cache-setting.d.ts +2 -0
- package/dist/utils/get-cache-setting.d.ts.map +1 -0
- package/dist/utils/get-cache-setting.js +6 -0
- package/dist/utils/get-cache-setting.js.map +1 -0
- package/dist/utils/get-cached-metadata.d.ts +18 -0
- package/dist/utils/get-cached-metadata.d.ts.map +1 -0
- package/dist/utils/get-cached-metadata.js +47 -0
- package/dist/utils/get-cached-metadata.js.map +1 -0
- package/dist/utils/get-mfe-has-compatible-launchdarkly-version.d.ts +1 -1
- package/dist/utils/get-mfe-has-compatible-launchdarkly-version.d.ts.map +1 -1
- package/dist/utils/get-mfe-has-compatible-launchdarkly-version.js.map +1 -1
- package/dist/utils/get-mfe-has-unique-chunk-namespace.d.ts +1 -1
- package/dist/utils/get-mfe-has-unique-chunk-namespace.d.ts.map +1 -1
- package/dist/utils/get-mfe-has-unique-chunk-namespace.js.map +1 -1
- package/dist/utils/get-mfe-supports-rerendering.d.ts +1 -1
- package/dist/utils/get-mfe-supports-rerendering.d.ts.map +1 -1
- package/dist/utils/get-mfe-supports-rerendering.js.map +1 -1
- package/dist/utils/get-version-mismatches.d.ts +1 -1
- package/dist/utils/get-version-mismatches.d.ts.map +1 -1
- package/dist/utils/get-version-mismatches.js +1 -1
- package/dist/utils/get-version-mismatches.js.map +1 -1
- package/dist/utils/web-component.d.ts +1 -1
- package/dist/utils/web-component.d.ts.map +1 -1
- package/dist/utils/web-component.js +12 -9
- package/dist/utils/web-component.js.map +1 -1
- package/package.json +12 -12
- package/src/__tests__/headless-loader.test.tsx +123 -0
- package/src/__tests__/loader.test.tsx +33 -11
- package/src/__tests__/register-headless.test.tsx +98 -0
- package/src/headless-loader.tsx +21 -0
- package/src/index.ts +2 -0
- package/src/loader.tsx +58 -46
- package/src/register-headless.ts +81 -0
- package/src/types.ts +9 -0
- package/src/utils/__tests__/get-version-mismatches.test.ts +1 -1
- package/src/utils/get-bundle-info.ts +37 -60
- package/src/utils/get-cache-setting.ts +5 -0
- package/src/utils/get-cached-metadata.ts +72 -0
- package/src/utils/get-mfe-has-compatible-launchdarkly-version.ts +1 -1
- package/src/utils/get-mfe-has-unique-chunk-namespace.ts +1 -1
- package/src/utils/get-mfe-supports-rerendering.ts +1 -1
- package/src/utils/get-version-mismatches.ts +1 -1
- package/src/utils/web-component.tsx +8 -6
- package/dist/__mocks__/expect-styles-in-dom.d.ts +0 -3
- package/dist/__mocks__/expect-styles-in-dom.d.ts.map +0 -1
- package/dist/__mocks__/index.d.ts +0 -4
- package/dist/__mocks__/index.d.ts.map +0 -1
- package/dist/__mocks__/register-component.d.ts +0 -4
- package/dist/__mocks__/register-component.d.ts.map +0 -1
- package/dist/__mocks__/test-component.d.ts +0 -3
- 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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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
|
-
|
|
63
|
-
const { cache =
|
|
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
|
-
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
+
}
|