@servicetitan/web-components 23.0.0 → 23.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/__mocks__/index.d.ts +3 -0
- package/dist/__mocks__/index.d.ts.map +1 -0
- package/dist/__mocks__/index.js +3 -0
- package/dist/__mocks__/index.js.map +1 -0
- package/dist/__mocks__/register-component.d.ts +4 -0
- package/dist/__mocks__/register-component.d.ts.map +1 -0
- package/dist/__mocks__/register-component.js +14 -0
- package/dist/__mocks__/register-component.js.map +1 -0
- package/dist/__mocks__/test-component.d.ts +3 -0
- package/dist/__mocks__/test-component.d.ts.map +1 -0
- package/dist/__mocks__/test-component.js +14 -0
- package/dist/__mocks__/test-component.js.map +1 -0
- package/dist/contexts/tests/mfe-data-context.test.js +1 -1
- package/dist/contexts/tests/mfe-data-context.test.js.map +1 -1
- package/dist/contexts/tests/mfe-metadata-context.test.js +1 -1
- package/dist/contexts/tests/mfe-metadata-context.test.js.map +1 -1
- package/dist/loader.d.ts +7 -17
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +74 -224
- package/dist/loader.js.map +1 -1
- package/dist/register.d.ts +4 -1
- package/dist/register.d.ts.map +1 -1
- package/dist/register.js +26 -13
- package/dist/register.js.map +1 -1
- package/dist/render.d.ts +11 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +39 -0
- package/dist/render.js.map +1 -0
- package/dist/utils/get-bundle-info.d.ts +24 -0
- package/dist/utils/get-bundle-info.d.ts.map +1 -0
- package/dist/utils/get-bundle-info.js +98 -0
- package/dist/utils/get-bundle-info.js.map +1 -0
- package/dist/utils/get-entry-points.d.ts +6 -0
- package/dist/utils/get-entry-points.d.ts.map +1 -0
- package/dist/utils/get-entry-points.js +24 -0
- package/dist/utils/get-entry-points.js.map +1 -0
- package/dist/{get-mfe-supports-rerendering.d.ts → utils/get-mfe-supports-rerendering.d.ts} +1 -1
- package/dist/utils/get-mfe-supports-rerendering.d.ts.map +1 -0
- package/dist/utils/get-mfe-supports-rerendering.js.map +1 -0
- package/dist/{utils.d.ts → utils/get-version-mismatches.d.ts} +2 -3
- package/dist/utils/get-version-mismatches.d.ts.map +1 -0
- package/dist/{utils.js → utils/get-version-mismatches.js} +2 -10
- package/dist/utils/get-version-mismatches.js.map +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/is-versioned-url.d.ts +2 -0
- package/dist/utils/is-versioned-url.d.ts.map +1 -0
- package/dist/utils/is-versioned-url.js +9 -0
- package/dist/utils/is-versioned-url.js.map +1 -0
- package/dist/utils/load-scripts.d.ts +3 -0
- package/dist/utils/load-scripts.d.ts.map +1 -0
- package/dist/utils/load-scripts.js +31 -0
- package/dist/utils/load-scripts.js.map +1 -0
- package/dist/utils/validate-bundle-info.d.ts +7 -0
- package/dist/utils/validate-bundle-info.d.ts.map +1 -0
- package/dist/utils/validate-bundle-info.js +10 -0
- package/dist/utils/validate-bundle-info.js.map +1 -0
- package/dist/utils/web-component.d.ts +17 -0
- package/dist/utils/web-component.d.ts.map +1 -0
- package/dist/utils/web-component.js +54 -0
- package/dist/utils/web-component.js.map +1 -0
- package/package.json +17 -18
- package/src/__mocks__/index.ts +2 -0
- package/src/__mocks__/register-component.tsx +20 -0
- package/src/__mocks__/test-component.tsx +30 -0
- package/src/__tests__/event-bus.test.ts +3 -2
- package/src/__tests__/loader.test.tsx +17 -11
- package/src/__tests__/register-legacy-root.test.tsx +36 -0
- package/src/__tests__/register.test.tsx +16 -81
- package/src/contexts/tests/mfe-data-context.test.tsx +4 -4
- package/src/contexts/tests/mfe-metadata-context.test.tsx +4 -4
- package/src/loader.tsx +129 -374
- package/src/register.tsx +25 -14
- package/src/render.ts +62 -0
- package/src/{__tests__ → utils/__tests__}/get-mfe-supports-rerendering.test.ts +1 -1
- package/src/utils/__tests__/get-version-mismatches.test.ts +76 -0
- package/src/utils/__tests__/is-versioned-url.test.ts +18 -0
- package/src/utils/get-bundle-info.ts +121 -0
- package/src/utils/get-entry-points.ts +23 -0
- package/src/{get-mfe-supports-rerendering.ts → utils/get-mfe-supports-rerendering.ts} +1 -1
- package/src/{utils.ts → utils/get-version-mismatches.ts} +1 -12
- package/src/utils/index.ts +8 -0
- package/src/utils/is-versioned-url.ts +11 -0
- package/src/utils/load-scripts.ts +38 -0
- package/src/utils/validate-bundle-info.ts +15 -0
- package/src/utils/web-component.tsx +96 -0
- package/dist/get-mfe-supports-rerendering.d.ts.map +0 -1
- package/dist/get-mfe-supports-rerendering.js.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/__tests__/utils.test.ts +0 -95
- /package/dist/{get-mfe-supports-rerendering.js → utils/get-mfe-supports-rerendering.js} +0 -0
package/src/render.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* eslint-disable react/no-deprecated */
|
|
2
|
+
import { ReactElement } from 'react';
|
|
3
|
+
import ReactDOM from 'react-dom';
|
|
4
|
+
import type ReactDOMClient from 'react-dom/client';
|
|
5
|
+
|
|
6
|
+
let createRoot: typeof ReactDOMClient.createRoot;
|
|
7
|
+
try {
|
|
8
|
+
/**
|
|
9
|
+
* Note, the webpack.IgnorePlugin must be configured to ignore this dependency.
|
|
10
|
+
* @see {@link file://./../../startup/src/webpack/configs/plugins/ignore-plugin/ignore-plugin.ts}
|
|
11
|
+
*/
|
|
12
|
+
createRoot = require('react-dom/client').createRoot;
|
|
13
|
+
} catch (e) {
|
|
14
|
+
// ignore
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RenderOptions {
|
|
18
|
+
legacyRoot?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface RenderResult {
|
|
22
|
+
rerender(element: ReactElement): void;
|
|
23
|
+
unmount(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function render(
|
|
27
|
+
element: ReactElement,
|
|
28
|
+
container: Element | DocumentFragment,
|
|
29
|
+
options?: RenderOptions
|
|
30
|
+
): RenderResult {
|
|
31
|
+
if (typeof createRoot !== 'function' || options?.legacyRoot) {
|
|
32
|
+
return renderLegacyRoot(element, container);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const root = createRoot(container);
|
|
36
|
+
root.render(element);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
rerender(element: ReactElement) {
|
|
40
|
+
root.render(element);
|
|
41
|
+
},
|
|
42
|
+
unmount() {
|
|
43
|
+
root.unmount();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderLegacyRoot(
|
|
49
|
+
element: ReactElement,
|
|
50
|
+
container: Element | DocumentFragment
|
|
51
|
+
): RenderResult {
|
|
52
|
+
ReactDOM.render(element, container);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
rerender(element: ReactElement) {
|
|
56
|
+
ReactDOM.render(element, container);
|
|
57
|
+
},
|
|
58
|
+
unmount() {
|
|
59
|
+
ReactDOM.unmountComponentAtNode(container);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getMFESupportsRerendering } from '../get-mfe-supports-rerendering';
|
|
2
|
-
import { Metadata } from '../
|
|
2
|
+
import { Metadata } from '../get-bundle-info';
|
|
3
3
|
|
|
4
4
|
describe(`[web-components] ${getMFESupportsRerendering.name}`, () => {
|
|
5
5
|
let metadata: Required<Pick<Metadata, 'bundledWith' | 'dependencies'>>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ExposedDependencies } from '../../common';
|
|
2
|
+
import { getVersionMismatches } from '../get-version-mismatches';
|
|
3
|
+
|
|
4
|
+
describe(`[web-components] ${getVersionMismatches.name}`, () => {
|
|
5
|
+
let sharedDependencies: Record<string, string> = {};
|
|
6
|
+
let dependencies: Record<string, string> = {};
|
|
7
|
+
let exposedDependencies: ExposedDependencies = {};
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
sharedDependencies = {
|
|
11
|
+
'@servicetitan/design-system': 'SharedDependencies.ServiceTitan.DesignSystem',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
dependencies = {
|
|
15
|
+
'@servicetitan/design-system': '~11.8.0',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
exposedDependencies = {
|
|
19
|
+
'@servicetitan/design-system': {
|
|
20
|
+
version: '~11.8.0',
|
|
21
|
+
variable: 'SharedDependencies.ServiceTitan.DesignSystem',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('returns no mismatches', () => {
|
|
27
|
+
expect(getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)).toEqual(
|
|
28
|
+
{}
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('when there is a versions mismatch', () => {
|
|
33
|
+
beforeEach(() => (dependencies['@servicetitan/design-system'] = '~12.0.0'));
|
|
34
|
+
|
|
35
|
+
test('returns one mismatch', () => {
|
|
36
|
+
expect(
|
|
37
|
+
getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
|
|
38
|
+
).toEqual({
|
|
39
|
+
'@servicetitan/design-system': {
|
|
40
|
+
host: '~11.8.0',
|
|
41
|
+
package: '~12.0.0',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('when a dependency is missing from exposed dependencies', () => {
|
|
48
|
+
beforeEach(() => delete exposedDependencies['@servicetitan/design-system']);
|
|
49
|
+
|
|
50
|
+
test('returns an object citing the dependency is missing from the host', () => {
|
|
51
|
+
expect(
|
|
52
|
+
getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
|
|
53
|
+
).toEqual({
|
|
54
|
+
'@servicetitan/design-system': {
|
|
55
|
+
host: 'missing',
|
|
56
|
+
package: '~11.8.0',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('when a variable does not match', () => {
|
|
63
|
+
beforeEach(() => (exposedDependencies['@servicetitan/design-system'].variable = 'foo'));
|
|
64
|
+
|
|
65
|
+
test('returns an object citing the variable is wrong', () => {
|
|
66
|
+
expect(
|
|
67
|
+
getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
|
|
68
|
+
).toEqual({
|
|
69
|
+
'@servicetitan/design-system': {
|
|
70
|
+
host: 'wrong global variable',
|
|
71
|
+
package: '~11.8.0',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isVersionedUrl } from '../is-versioned-url';
|
|
2
|
+
|
|
3
|
+
describe(`${isVersionedUrl.name}`, () => {
|
|
4
|
+
const results: [string, boolean][] = [
|
|
5
|
+
['localhost:9090', false],
|
|
6
|
+
['localhost:9090@1.1.1', false],
|
|
7
|
+
['localhost:9090@dev', false],
|
|
8
|
+
['//unpkg.servicetitan.com/@servicetitan/test', false],
|
|
9
|
+
['//unpkg.servicetitan.com/@servicetitan/test@dev', false],
|
|
10
|
+
['//unpkg.servicetitan.com/@servicetitan/test@0.0.0', true],
|
|
11
|
+
['//unpkg.servicetitan.com/@servicetitan/test@0.0.0-hash', true],
|
|
12
|
+
['//unpkg.servicetitan.com/@servicetitan/test@^16', false],
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
test.each(results)(`providing %s returns %s`, (url, result) => {
|
|
16
|
+
expect(isVersionedUrl(url)).toEqual(result);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import get from 'lodash.get';
|
|
3
|
+
|
|
4
|
+
import { ExposedDependencies, getJson, GetJsonError } from '../common';
|
|
5
|
+
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
|
+
|
|
10
|
+
export interface Metadata {
|
|
11
|
+
name: string;
|
|
12
|
+
bundledWith?: Record<string, string | undefined>;
|
|
13
|
+
sharedDependencies: Record<string, string>;
|
|
14
|
+
dependencies?: Record<string, string>;
|
|
15
|
+
entrypoints?: Record<string, { css: string[]; js: string[] }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type BundleInfo = Awaited<ReturnType<typeof getBundleInfo>>;
|
|
19
|
+
|
|
20
|
+
export async function getBundleInfo(
|
|
21
|
+
mainPackageUrl: string,
|
|
22
|
+
fallbackPackageUrl: string | undefined,
|
|
23
|
+
exposedDependencies: ExposedDependencies = {},
|
|
24
|
+
signal?: AbortSignal,
|
|
25
|
+
retries?: number,
|
|
26
|
+
onRequestError?: (error: GetJsonError) => void
|
|
27
|
+
) {
|
|
28
|
+
const { data: metadata, url: packageUrl } = await getMetadata({
|
|
29
|
+
fallbackPackageUrl,
|
|
30
|
+
mainPackageUrl,
|
|
31
|
+
onRequestError,
|
|
32
|
+
retries,
|
|
33
|
+
signal,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const { bundledWith, name, sharedDependencies } = metadata;
|
|
37
|
+
const exactPackageUrl = packageUrl.replace('/dist/metadata.json', '');
|
|
38
|
+
const cache = getCacheSetting(exactPackageUrl);
|
|
39
|
+
|
|
40
|
+
let dependencies: NonNullable<Metadata['dependencies']>;
|
|
41
|
+
if (metadata.dependencies) {
|
|
42
|
+
dependencies = metadata.dependencies;
|
|
43
|
+
} else {
|
|
44
|
+
const { data: packageJson } = await getJson(`${exactPackageUrl}/package.json`, {
|
|
45
|
+
cache,
|
|
46
|
+
retries,
|
|
47
|
+
signal,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
dependencies = packageJson.dependencies;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const mismatch = getVersionMismatches(exposedDependencies, dependencies, sharedDependencies);
|
|
54
|
+
const hasMismatch = !!Object.keys(mismatch).length;
|
|
55
|
+
const startupVersion = bundledWith?.['@servicetitan/startup'];
|
|
56
|
+
const supportLightBundles = startupVersion && semver.gte(startupVersion, '17.0.0');
|
|
57
|
+
const embed = !hasMismatch && supportLightBundles;
|
|
58
|
+
const bundleType = embed ? 'light' : 'full';
|
|
59
|
+
const withBaseUrl = (path: string) => `${exactPackageUrl}/dist/bundle/${bundleType}/${path}`;
|
|
60
|
+
|
|
61
|
+
let entrypoints: EntryPoints;
|
|
62
|
+
if (metadata.entrypoints?.[bundleType]) {
|
|
63
|
+
entrypoints = metadata.entrypoints[bundleType];
|
|
64
|
+
} else {
|
|
65
|
+
entrypoints = await getEntrypoints(withBaseUrl('entrypoints.json'), cache);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const disposer = () => {
|
|
69
|
+
if (embed) {
|
|
70
|
+
const path = Object.values<string>(sharedDependencies).shift();
|
|
71
|
+
if (path) {
|
|
72
|
+
get(window, path).getStorage().clear(name);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const urls = Object.entries(entrypoints).reduce(
|
|
78
|
+
(result, [key, value]) => ({ ...result, [key]: value.map(withBaseUrl) }),
|
|
79
|
+
{} as EntryPoints
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
disposer,
|
|
84
|
+
mfeSupportsRerendering: getMFESupportsRerendering(bundledWith, dependencies),
|
|
85
|
+
mismatch: hasMismatch ? mismatch : undefined,
|
|
86
|
+
urls,
|
|
87
|
+
WebComponent: name,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function getMetadata(props: {
|
|
92
|
+
fallbackPackageUrl?: string;
|
|
93
|
+
mainPackageUrl: string;
|
|
94
|
+
onRequestError?: (error: GetJsonError) => void;
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
retries?: number;
|
|
97
|
+
}) {
|
|
98
|
+
const { fallbackPackageUrl, mainPackageUrl, onRequestError, retries, signal } = props;
|
|
99
|
+
|
|
100
|
+
return getJson<Metadata>(`${mainPackageUrl}/dist/metadata.json`, {
|
|
101
|
+
cache: getCacheSetting(mainPackageUrl),
|
|
102
|
+
retries: fallbackPackageUrl ? 0 : retries,
|
|
103
|
+
onRequestError,
|
|
104
|
+
signal,
|
|
105
|
+
}).catch(e => {
|
|
106
|
+
if (!fallbackPackageUrl) {
|
|
107
|
+
throw e;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return getJson(`${fallbackPackageUrl}/dist/metadata.json`, {
|
|
111
|
+
retries,
|
|
112
|
+
onRequestError,
|
|
113
|
+
cache: getCacheSetting(fallbackPackageUrl),
|
|
114
|
+
signal,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getCacheSetting(url: string) {
|
|
120
|
+
return isVersionedUrl(url) ? undefined : 'no-store';
|
|
121
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getJson } from '../common';
|
|
2
|
+
|
|
3
|
+
export interface EntryPoints {
|
|
4
|
+
css: string[];
|
|
5
|
+
js: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function getEntrypoints(
|
|
9
|
+
url: string,
|
|
10
|
+
cache: 'no-store' | undefined,
|
|
11
|
+
signal?: AbortSignal
|
|
12
|
+
): Promise<EntryPoints> {
|
|
13
|
+
try {
|
|
14
|
+
return (
|
|
15
|
+
await getJson(url, {
|
|
16
|
+
cache,
|
|
17
|
+
signal,
|
|
18
|
+
})
|
|
19
|
+
).data;
|
|
20
|
+
} catch {
|
|
21
|
+
return { css: ['index.css'], js: ['index.js'] };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { ExposedDependencies, isCompatible } from './common';
|
|
1
|
+
import { ExposedDependencies, isCompatible } from '../common';
|
|
3
2
|
|
|
4
3
|
export const getVersionMismatches = (
|
|
5
4
|
exposedDependencies: ExposedDependencies,
|
|
@@ -31,13 +30,3 @@ export const getVersionMismatches = (
|
|
|
31
30
|
|
|
32
31
|
return mismatch;
|
|
33
32
|
};
|
|
34
|
-
|
|
35
|
-
export const isVersionedUrl = (url: string): boolean => {
|
|
36
|
-
if (!url.includes('unpkg.')) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const parts = url.split('@');
|
|
41
|
-
|
|
42
|
-
return !!semver.clean(parts[parts.length - 1]);
|
|
43
|
-
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './get-bundle-info';
|
|
2
|
+
export * from './get-entry-points';
|
|
3
|
+
export * from './get-mfe-supports-rerendering';
|
|
4
|
+
export * from './get-version-mismatches';
|
|
5
|
+
export * from './is-versioned-url';
|
|
6
|
+
export * from './load-scripts';
|
|
7
|
+
export * from './validate-bundle-info';
|
|
8
|
+
export * from './web-component';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BundleInfo } from './get-bundle-info';
|
|
2
|
+
|
|
3
|
+
export function loadScripts({ urls, WebComponent }: BundleInfo) {
|
|
4
|
+
return Promise.all(
|
|
5
|
+
urls.js.map(url => {
|
|
6
|
+
const script = Array.from(document.scripts).find(({ src }) => src === url);
|
|
7
|
+
if (!script) {
|
|
8
|
+
return new Promise<void>(resolve => {
|
|
9
|
+
const script = document.createElement('script');
|
|
10
|
+
|
|
11
|
+
script.crossOrigin = 'anonymous';
|
|
12
|
+
script.onload = () => {
|
|
13
|
+
script.dataset.webComponent = WebComponent;
|
|
14
|
+
script.onload = null;
|
|
15
|
+
resolve();
|
|
16
|
+
};
|
|
17
|
+
script.async = true;
|
|
18
|
+
script.src = url;
|
|
19
|
+
|
|
20
|
+
document.body.append(script);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!script.dataset.webComponent) {
|
|
25
|
+
return new Promise<void>(resolve => {
|
|
26
|
+
const originalOnload = script.onload;
|
|
27
|
+
script.onload = (...args) => {
|
|
28
|
+
originalOnload?.call(script, ...args);
|
|
29
|
+
script.onload = null;
|
|
30
|
+
resolve();
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Promise.resolve();
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Log } from '@servicetitan/log-service';
|
|
2
|
+
import { BundleInfo } from './get-bundle-info';
|
|
3
|
+
|
|
4
|
+
export function validateBundleInfo(
|
|
5
|
+
{ mismatch }: BundleInfo,
|
|
6
|
+
{ logService, packageName }: { logService?: Log; packageName: string }
|
|
7
|
+
) {
|
|
8
|
+
if (mismatch) {
|
|
9
|
+
logService?.warning({
|
|
10
|
+
category: 'MicroFrontends.DependenciesMismatch',
|
|
11
|
+
message: 'Some of the package dependencies have incompatible versions.',
|
|
12
|
+
data: { package: packageName, dependencies: JSON.stringify(mismatch) },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Fragment, useMemo, useState } from 'react';
|
|
2
|
+
import { Log } from '@servicetitan/log-service';
|
|
3
|
+
import { useOptionalDependencies } from '@servicetitan/react-ioc';
|
|
4
|
+
|
|
5
|
+
import { ExposedDependencies, IWebComponent } from '../common';
|
|
6
|
+
import { MFEContextData } from '../contexts/mfe-data-context';
|
|
7
|
+
import { EventBus } from '../event-bus';
|
|
8
|
+
import { BundleInfo } from './get-bundle-info';
|
|
9
|
+
import { HistoryManager } from '../history-manager';
|
|
10
|
+
import { LoaderDataProps } from '../loader';
|
|
11
|
+
|
|
12
|
+
interface WebComponentProps {
|
|
13
|
+
basename?: string;
|
|
14
|
+
bundleInfo: BundleInfo;
|
|
15
|
+
eventBus: EventBus;
|
|
16
|
+
exposedDependencies?: ExposedDependencies;
|
|
17
|
+
logService?: Log;
|
|
18
|
+
LoadingFallback: () => JSX.Element;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function webComponent(props: WebComponentProps) {
|
|
22
|
+
const { basename, bundleInfo, eventBus, exposedDependencies, logService, LoadingFallback } =
|
|
23
|
+
props;
|
|
24
|
+
const { WebComponent, disposer, mfeSupportsRerendering } = bundleInfo;
|
|
25
|
+
|
|
26
|
+
return (props: LoaderDataProps) => {
|
|
27
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
28
|
+
const [historyManager] = useOptionalDependencies(HistoryManager);
|
|
29
|
+
|
|
30
|
+
const handleRef = (webComponent: IWebComponent | null) => {
|
|
31
|
+
webComponent?.provide({
|
|
32
|
+
logService,
|
|
33
|
+
historyManager,
|
|
34
|
+
exposedDependencies,
|
|
35
|
+
eventBus,
|
|
36
|
+
basename,
|
|
37
|
+
onReady: () => {
|
|
38
|
+
setIsLoading(false);
|
|
39
|
+
},
|
|
40
|
+
onDispose: () => {
|
|
41
|
+
disposer();
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const stringifiedData = useMemo(() => JSON.stringify(props.data), [props.data]);
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* Set key for rendering the web component so that if the data from the Host change,
|
|
50
|
+
* and the MFE's version of startup doesn't support watching for data changes, then
|
|
51
|
+
* the web component will be re-mounted whenever the data changes.
|
|
52
|
+
* This is only used as a fallback when the MFE's version is older.
|
|
53
|
+
*/
|
|
54
|
+
const key = mfeSupportsRerendering ? undefined : stringifiedData;
|
|
55
|
+
const dataAttrs = useMemo(() => composeDataAttributes(props.data), [props.data]);
|
|
56
|
+
const styleUrls = getStyleUrls(bundleInfo);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Fragment>
|
|
60
|
+
{isLoading && <LoadingFallback />}
|
|
61
|
+
<WebComponent
|
|
62
|
+
key={key}
|
|
63
|
+
ref={handleRef}
|
|
64
|
+
class={props.className}
|
|
65
|
+
{...(styleUrls && { 'style-urls': styleUrls })}
|
|
66
|
+
/*
|
|
67
|
+
* Continue to spread data as separate data attributes for backwards compatibility,
|
|
68
|
+
* in case the Host has updated web-components, but the MFE has not
|
|
69
|
+
*/
|
|
70
|
+
{...dataAttrs}
|
|
71
|
+
// Stringify data object so that it can be passed as a data-attribute
|
|
72
|
+
data-mfe-data={stringifiedData}
|
|
73
|
+
/>
|
|
74
|
+
</Fragment>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/*
|
|
80
|
+
* Convert camelCase data props to kebab-case data attributes, prepended with data-
|
|
81
|
+
* Ex: fooBar -> data-foo-bar
|
|
82
|
+
*/
|
|
83
|
+
function composeDataAttributes(data: MFEContextData<{}> | undefined = {}) {
|
|
84
|
+
return Object.fromEntries(
|
|
85
|
+
Object.entries(data).map(([key, value]) => [
|
|
86
|
+
'data-' + key.replace(/[A-Z]/g, letter => '-' + letter.toLowerCase()),
|
|
87
|
+
value,
|
|
88
|
+
])
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getStyleUrls({ urls }: Pick<BundleInfo, 'urls'>) {
|
|
93
|
+
if (urls.css.length) {
|
|
94
|
+
return encodeURIComponent(JSON.stringify(urls.css));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"get-mfe-supports-rerendering.d.ts","sourceRoot":"","sources":["../src/get-mfe-supports-rerendering.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAYpC,wBAAgB,yBAAyB,CACrC,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC,EACpC,YAAY,EAAE,QAAQ,CAAC,cAAc,CAAC,WAOzC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"get-mfe-supports-rerendering.js","sourceRoot":"","sources":["../src/get-mfe-supports-rerendering.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAG5B;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CACrC,WAAoC,EACpC,YAAsC;;IAEtC,OAAO,CAAC,CAAC,CACL,MAAA,MAAA,kBAAkB,CAAC,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAG,8BAA8B,CAAC,CAAC,mCACjE,kBAAkB,CAAC,YAAY,aAAZ,YAAY,uBAAZ,YAAY,CAAG,8BAA8B,CAAC,CAAC,mCAClE,kBAAkB,CAAC,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAG,uBAAuB,CAAC,CAAC,CAC7D,CAAC;AACN,CAAC;AAED,SAAS,kBAAkB,CAAC,OAAgB;IACxC,MAAM,UAAU,GAAG,OAAO,IAAI,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IACzD,IAAI,UAAU,EAAE;QACZ,OAAO,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;KAC5C;AACL,CAAC"}
|
package/dist/utils.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAgB,MAAM,UAAU,CAAC;AAE7D,eAAO,MAAM,oBAAoB,wBACR,mBAAmB,gBAC1B,OAAO,MAAM,EAAE,MAAM,CAAC,sBAChB,OAAO,MAAM,EAAE,MAAM,CAAC;UAYT,MAAM;aAAW,MAAM;EAc3D,CAAC;AAEF,eAAO,MAAM,cAAc,QAAS,MAAM,KAAG,OAQ5C,CAAC"}
|
package/dist/utils.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAuB,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAChC,mBAAwC,EACxC,YAAoC,EACpC,kBAA0C,EAC5C,EAAE;IACA,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC,MAAM,CACvD,CAAC,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE;QAC/C,MAAM,cAAc,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;QAEhD,IAAI,cAAc,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,cAAc,CAAC,EAAE;YAC9D,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC;SACvE;QAED,OAAO,MAAM,CAAC;IAClB,CAAC,EACD,EAAuD,CAC1D,CAAC;IAEF,KAAK,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE;QACrE,MAAM,cAAc,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;QAEhD,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE;YAClC,QAAQ,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC;SACvE;aAAM,IAAI,mBAAmB,CAAC,UAAU,CAAC,CAAC,QAAQ,KAAK,QAAQ,EAAE;YAC9D,QAAQ,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC;SACrF;KACJ;IAED,OAAO,QAAQ,CAAC;AACpB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAW,EAAW,EAAE;IACnD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;QACzB,OAAO,KAAK,CAAC;KAChB;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE7B,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;AACnD,CAAC,CAAC"}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { ExposedDependencies } from '../common';
|
|
2
|
-
import { getVersionMismatches, isVersionedUrl } from '../utils';
|
|
3
|
-
|
|
4
|
-
describe('[web-components] utils', () => {
|
|
5
|
-
describe(`${getVersionMismatches.name}`, () => {
|
|
6
|
-
let sharedDependencies: Record<string, string> = {};
|
|
7
|
-
let dependencies: Record<string, string> = {};
|
|
8
|
-
let exposedDependencies: ExposedDependencies = {};
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
sharedDependencies = {
|
|
12
|
-
'@servicetitan/design-system': 'SharedDependencies.ServiceTitan.DesignSystem',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
dependencies = {
|
|
16
|
-
'@servicetitan/design-system': '~11.8.0',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
exposedDependencies = {
|
|
20
|
-
'@servicetitan/design-system': {
|
|
21
|
-
version: '~11.8.0',
|
|
22
|
-
variable: 'SharedDependencies.ServiceTitan.DesignSystem',
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('returns no mismatches', () => {
|
|
28
|
-
expect(
|
|
29
|
-
getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
|
|
30
|
-
).toEqual({});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('when there is a versions mismatch', () => {
|
|
34
|
-
beforeEach(() => (dependencies['@servicetitan/design-system'] = '~12.0.0'));
|
|
35
|
-
|
|
36
|
-
test('returns one mismatch', () => {
|
|
37
|
-
expect(
|
|
38
|
-
getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
|
|
39
|
-
).toEqual({
|
|
40
|
-
'@servicetitan/design-system': {
|
|
41
|
-
host: '~11.8.0',
|
|
42
|
-
package: '~12.0.0',
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('when a dependency is missing from exposed dependencies', () => {
|
|
49
|
-
beforeEach(() => delete exposedDependencies['@servicetitan/design-system']);
|
|
50
|
-
|
|
51
|
-
test('returns an object citing the dependency is missing from the host', () => {
|
|
52
|
-
expect(
|
|
53
|
-
getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
|
|
54
|
-
).toEqual({
|
|
55
|
-
'@servicetitan/design-system': {
|
|
56
|
-
host: 'missing',
|
|
57
|
-
package: '~11.8.0',
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('when a variable does not match', () => {
|
|
64
|
-
beforeEach(() => (exposedDependencies['@servicetitan/design-system'].variable = 'foo'));
|
|
65
|
-
|
|
66
|
-
test('returns an object citing the variable is wrong', () => {
|
|
67
|
-
expect(
|
|
68
|
-
getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
|
|
69
|
-
).toEqual({
|
|
70
|
-
'@servicetitan/design-system': {
|
|
71
|
-
host: 'wrong global variable',
|
|
72
|
-
package: '~11.8.0',
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe(`${isVersionedUrl.name}`, () => {
|
|
80
|
-
const results: [string, boolean][] = [
|
|
81
|
-
['localhost:9090', false],
|
|
82
|
-
['localhost:9090@1.1.1', false],
|
|
83
|
-
['localhost:9090@dev', false],
|
|
84
|
-
['//unpkg.servicetitan.com/@servicetitan/test', false],
|
|
85
|
-
['//unpkg.servicetitan.com/@servicetitan/test@dev', false],
|
|
86
|
-
['//unpkg.servicetitan.com/@servicetitan/test@0.0.0', true],
|
|
87
|
-
['//unpkg.servicetitan.com/@servicetitan/test@0.0.0-hash', true],
|
|
88
|
-
['//unpkg.servicetitan.com/@servicetitan/test@^16', false],
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
test.each(results)(`providing %s returns %s`, (url, result) => {
|
|
92
|
-
expect(isVersionedUrl(url)).toEqual(result);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
});
|
|
File without changes
|