@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.
Files changed (94) hide show
  1. package/dist/__mocks__/index.d.ts +3 -0
  2. package/dist/__mocks__/index.d.ts.map +1 -0
  3. package/dist/__mocks__/index.js +3 -0
  4. package/dist/__mocks__/index.js.map +1 -0
  5. package/dist/__mocks__/register-component.d.ts +4 -0
  6. package/dist/__mocks__/register-component.d.ts.map +1 -0
  7. package/dist/__mocks__/register-component.js +14 -0
  8. package/dist/__mocks__/register-component.js.map +1 -0
  9. package/dist/__mocks__/test-component.d.ts +3 -0
  10. package/dist/__mocks__/test-component.d.ts.map +1 -0
  11. package/dist/__mocks__/test-component.js +14 -0
  12. package/dist/__mocks__/test-component.js.map +1 -0
  13. package/dist/contexts/tests/mfe-data-context.test.js +1 -1
  14. package/dist/contexts/tests/mfe-data-context.test.js.map +1 -1
  15. package/dist/contexts/tests/mfe-metadata-context.test.js +1 -1
  16. package/dist/contexts/tests/mfe-metadata-context.test.js.map +1 -1
  17. package/dist/loader.d.ts +7 -17
  18. package/dist/loader.d.ts.map +1 -1
  19. package/dist/loader.js +74 -224
  20. package/dist/loader.js.map +1 -1
  21. package/dist/register.d.ts +4 -1
  22. package/dist/register.d.ts.map +1 -1
  23. package/dist/register.js +26 -13
  24. package/dist/register.js.map +1 -1
  25. package/dist/render.d.ts +11 -0
  26. package/dist/render.d.ts.map +1 -0
  27. package/dist/render.js +39 -0
  28. package/dist/render.js.map +1 -0
  29. package/dist/utils/get-bundle-info.d.ts +24 -0
  30. package/dist/utils/get-bundle-info.d.ts.map +1 -0
  31. package/dist/utils/get-bundle-info.js +98 -0
  32. package/dist/utils/get-bundle-info.js.map +1 -0
  33. package/dist/utils/get-entry-points.d.ts +6 -0
  34. package/dist/utils/get-entry-points.d.ts.map +1 -0
  35. package/dist/utils/get-entry-points.js +24 -0
  36. package/dist/utils/get-entry-points.js.map +1 -0
  37. package/dist/{get-mfe-supports-rerendering.d.ts → utils/get-mfe-supports-rerendering.d.ts} +1 -1
  38. package/dist/utils/get-mfe-supports-rerendering.d.ts.map +1 -0
  39. package/dist/utils/get-mfe-supports-rerendering.js.map +1 -0
  40. package/dist/{utils.d.ts → utils/get-version-mismatches.d.ts} +2 -3
  41. package/dist/utils/get-version-mismatches.d.ts.map +1 -0
  42. package/dist/{utils.js → utils/get-version-mismatches.js} +2 -10
  43. package/dist/utils/get-version-mismatches.js.map +1 -0
  44. package/dist/utils/index.d.ts +9 -0
  45. package/dist/utils/index.d.ts.map +1 -0
  46. package/dist/utils/index.js +9 -0
  47. package/dist/utils/index.js.map +1 -0
  48. package/dist/utils/is-versioned-url.d.ts +2 -0
  49. package/dist/utils/is-versioned-url.d.ts.map +1 -0
  50. package/dist/utils/is-versioned-url.js +9 -0
  51. package/dist/utils/is-versioned-url.js.map +1 -0
  52. package/dist/utils/load-scripts.d.ts +3 -0
  53. package/dist/utils/load-scripts.d.ts.map +1 -0
  54. package/dist/utils/load-scripts.js +31 -0
  55. package/dist/utils/load-scripts.js.map +1 -0
  56. package/dist/utils/validate-bundle-info.d.ts +7 -0
  57. package/dist/utils/validate-bundle-info.d.ts.map +1 -0
  58. package/dist/utils/validate-bundle-info.js +10 -0
  59. package/dist/utils/validate-bundle-info.js.map +1 -0
  60. package/dist/utils/web-component.d.ts +17 -0
  61. package/dist/utils/web-component.d.ts.map +1 -0
  62. package/dist/utils/web-component.js +54 -0
  63. package/dist/utils/web-component.js.map +1 -0
  64. package/package.json +17 -18
  65. package/src/__mocks__/index.ts +2 -0
  66. package/src/__mocks__/register-component.tsx +20 -0
  67. package/src/__mocks__/test-component.tsx +30 -0
  68. package/src/__tests__/event-bus.test.ts +3 -2
  69. package/src/__tests__/loader.test.tsx +17 -11
  70. package/src/__tests__/register-legacy-root.test.tsx +36 -0
  71. package/src/__tests__/register.test.tsx +16 -81
  72. package/src/contexts/tests/mfe-data-context.test.tsx +4 -4
  73. package/src/contexts/tests/mfe-metadata-context.test.tsx +4 -4
  74. package/src/loader.tsx +129 -374
  75. package/src/register.tsx +25 -14
  76. package/src/render.ts +62 -0
  77. package/src/{__tests__ → utils/__tests__}/get-mfe-supports-rerendering.test.ts +1 -1
  78. package/src/utils/__tests__/get-version-mismatches.test.ts +76 -0
  79. package/src/utils/__tests__/is-versioned-url.test.ts +18 -0
  80. package/src/utils/get-bundle-info.ts +121 -0
  81. package/src/utils/get-entry-points.ts +23 -0
  82. package/src/{get-mfe-supports-rerendering.ts → utils/get-mfe-supports-rerendering.ts} +1 -1
  83. package/src/{utils.ts → utils/get-version-mismatches.ts} +1 -12
  84. package/src/utils/index.ts +8 -0
  85. package/src/utils/is-versioned-url.ts +11 -0
  86. package/src/utils/load-scripts.ts +38 -0
  87. package/src/utils/validate-bundle-info.ts +15 -0
  88. package/src/utils/web-component.tsx +96 -0
  89. package/dist/get-mfe-supports-rerendering.d.ts.map +0 -1
  90. package/dist/get-mfe-supports-rerendering.js.map +0 -1
  91. package/dist/utils.d.ts.map +0 -1
  92. package/dist/utils.js.map +0 -1
  93. package/src/__tests__/utils.test.ts +0 -95
  94. /package/dist/{get-mfe-supports-rerendering.js → utils/get-mfe-supports-rerendering.js} +0 -0
package/src/loader.tsx CHANGED
@@ -1,425 +1,180 @@
1
1
  import {
2
- lazy,
2
+ ReactElement,
3
+ ReactNode,
3
4
  Suspense,
4
- useEffect,
5
- useMemo,
6
- useState,
7
5
  forwardRef,
6
+ lazy,
7
+ useEffect,
8
8
  useImperativeHandle,
9
- ReactElement,
10
- ReactNode,
11
- Ref,
9
+ useMemo,
12
10
  } from 'react';
13
-
14
- import semver from 'semver';
15
-
16
- import get from 'lodash.get';
17
-
18
11
  import { ErrorBoundary } from '@servicetitan/error-boundary';
19
12
  import { Log } from '@servicetitan/log-service';
20
13
  import { useOptionalDependencies } from '@servicetitan/react-ioc';
21
14
 
22
15
  import {
23
- ExposedDependencies,
24
16
  EXPOSED_DEPENDENCIES_TOKEN,
25
- useCancelation,
26
- getJson,
17
+ ExposedDependencies,
27
18
  GetJsonError,
28
- IWebComponent,
19
+ useCancelation,
29
20
  } from './common';
21
+ import { MFEContextData } from './contexts/mfe-data-context';
30
22
  import { EventBus, EVENT_BUS_TOKEN, useEventBus } from './event-bus';
31
- import { HistoryManager } from './history-manager';
32
23
  import { EXPOSED_DEPENDENCIES } from './globals';
33
- import { getVersionMismatches, isVersionedUrl } from './utils';
34
- import { MFEContextData } from './contexts/mfe-data-context';
35
- import { getMFESupportsRerendering } from './get-mfe-supports-rerendering';
36
-
37
- export interface Metadata {
38
- name: string;
39
- bundledWith?: Record<string, string | undefined>;
40
- sharedDependencies: Record<string, string>;
41
- dependencies?: Record<string, string>;
42
- entrypoints?: Record<string, { css: string[]; js: string[] }>;
43
- }
44
-
45
- interface EntryPoints {
46
- css: string[];
47
- js: string[];
48
- }
49
-
50
- async function getEntrypoints(
51
- url: string,
52
- cache: 'no-store' | undefined,
53
- signal?: AbortSignal
54
- ): Promise<EntryPoints> {
55
- try {
56
- return (
57
- await getJson(url, {
58
- cache,
59
- signal,
60
- })
61
- ).data;
62
- } catch {
63
- return { css: ['index.css'], js: ['index.js'] };
64
- }
65
- }
66
-
67
- async function getBundleInfo(
68
- mainPackageUrl: string,
69
- fallbackPackageUrl: string | undefined,
70
- exposedDependencies: ExposedDependencies = {},
71
- signal?: AbortSignal,
72
- retries?: number,
73
- onRequestError?: (error: GetJsonError) => void
74
- ) {
75
- const { data: metadata, url: packageUrl } = await getJson<Metadata>(
76
- `${mainPackageUrl}/dist/metadata.json`,
77
- {
78
- cache: isVersionedUrl(mainPackageUrl) ? undefined : 'no-store',
79
- retries: fallbackPackageUrl ? 0 : retries,
80
- onRequestError,
81
- signal,
82
- }
83
- ).catch(e => {
84
- if (!fallbackPackageUrl) {
85
- throw e;
86
- }
87
-
88
- return getJson(`${fallbackPackageUrl}/dist/metadata.json`, {
89
- retries,
90
- onRequestError,
91
- cache: isVersionedUrl(fallbackPackageUrl) ? undefined : 'no-store',
92
- signal,
93
- });
94
- });
95
-
96
- const exactPackageUrl = packageUrl.replace('/dist/metadata.json', '');
97
- const cache = isVersionedUrl(exactPackageUrl) ? undefined : 'no-store';
98
-
99
- let dependencies: NonNullable<Metadata['dependencies']>;
100
-
101
- if (metadata.dependencies) {
102
- dependencies = metadata.dependencies;
103
- } else {
104
- const { data: packageJson } = await getJson(`${exactPackageUrl}/package.json`, {
105
- retries,
106
- cache,
107
- signal,
108
- });
109
-
110
- dependencies = packageJson.dependencies;
111
- }
112
-
113
- const mismatch = getVersionMismatches(
114
- exposedDependencies,
115
- dependencies,
116
- metadata.sharedDependencies
117
- );
118
-
119
- const hasMismatch = !!Object.keys(mismatch).length;
120
- const startupVersion = metadata.bundledWith?.['@servicetitan/startup'];
121
- const supportLightBundles = startupVersion && semver.gte(startupVersion, '17.0.0');
122
- const embed = !hasMismatch && supportLightBundles;
123
- const bundleType = embed ? 'light' : 'full';
124
- const withBaseUrl = (path: string) => {
125
- return `${exactPackageUrl}/dist/bundle/${bundleType}/${path}`;
126
- };
127
-
128
- let entrypoints: EntryPoints;
129
-
130
- if (metadata.entrypoints?.[bundleType]) {
131
- entrypoints = metadata.entrypoints[bundleType];
132
- } else {
133
- entrypoints = await getEntrypoints(withBaseUrl('entrypoints.json'), cache);
134
- }
135
-
136
- return {
137
- urls: {
138
- css: entrypoints.css.map(withBaseUrl),
139
- js: entrypoints.js.map(withBaseUrl),
140
- },
141
- WebComponent: metadata.name,
142
- disposer: () => {
143
- if (embed) {
144
- const path = Object.values<string>(metadata.sharedDependencies).shift();
145
- if (path) {
146
- get(window, path).getStorage().clear(metadata.name);
147
- }
148
- }
149
- },
150
- mismatch: hasMismatch ? mismatch : undefined,
151
- mfeSupportsRerendering: getMFESupportsRerendering(metadata.bundledWith, dependencies),
152
- };
153
- }
24
+ import { BundleInfo, getBundleInfo, validateBundleInfo, loadScripts, webComponent } from './utils';
154
25
 
155
26
  export type LoaderRef = EventBus;
156
27
 
28
+ export type FallbackNode = NonNullable<ReactNode> | null;
29
+
157
30
  export interface LoaderProps<T> {
158
31
  src: string;
159
32
  fallbackSrc?: string;
160
33
  /** Values in the object will be stringified and then parsed before being delivered to the MFE via props or context. */
161
34
  data?: MFEContextData<T>;
162
35
  basename?: string;
163
- loadingFallback?: NonNullable<ReactNode> | null;
164
- errorFallback?: NonNullable<ReactNode> | null;
36
+ loadingFallback?: FallbackNode;
37
+ errorFallback?: FallbackNode;
165
38
  className?: string;
166
39
  retries?: number;
167
40
  onRequestError?(error: GetJsonError): boolean;
168
41
  }
169
42
 
170
- type DataProps = Pick<LoaderProps<{}>, 'data' | 'className'>;
43
+ export type LoaderDataProps = Pick<LoaderProps<{}>, 'data' | 'className'>;
171
44
 
172
- // Type complexity below is required to pass a generic type through forwardRef
173
- export const Loader: <T>(props: LoaderProps<T> & { ref?: Ref<LoaderRef> }) => ReactElement | null =
174
- forwardRef<LoaderRef, LoaderProps<{}>>(
175
- (
176
- {
177
- src,
178
- fallbackSrc,
179
- data = {},
180
- basename,
181
- loadingFallback,
182
- errorFallback,
183
- className,
184
- retries,
185
- onRequestError,
186
- },
187
- ref
188
- ) => {
189
- const [isLoading, setIsLoading] = useState(true);
190
-
191
- const bus = useEventBus();
192
- const [
193
- logService,
194
- historyManager,
195
- exposedDependencies = EXPOSED_DEPENDENCIES,
196
- eventBus = bus,
197
- ] = useOptionalDependencies(
198
- Log,
199
- HistoryManager,
200
- EXPOSED_DEPENDENCIES_TOKEN,
201
- EVENT_BUS_TOKEN
202
- );
203
-
204
- const Loader = useMemo(() => {
205
- const Component = forwardRef<LoaderRef, DataProps>((props: DataProps, ref) => {
206
- const cancelation = useCancelation();
207
-
208
- useImperativeHandle(ref, () => eventBus, []);
209
-
210
- useEffect(() => {
211
- setIsLoading(true);
212
-
213
- return () => {
214
- cancelation.abort();
215
- };
216
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
217
-
218
- const Lazy = useMemo(
219
- () =>
220
- lazy<(props: DataProps) => JSX.Element | null>(async () => {
221
- let bundleInfo: Awaited<ReturnType<typeof getBundleInfo>>;
222
- try {
223
- bundleInfo = await getBundleInfo(
224
- src,
225
- fallbackSrc,
226
- exposedDependencies,
227
- cancelation.signal,
228
- retries,
229
- onRequestError
230
- );
231
- } catch (e) {
232
- if (cancelation.signal.aborted) {
233
- return {
234
- default: () => null,
235
- };
236
- }
237
-
238
- throw e;
239
- }
240
-
241
- const {
242
- urls,
243
- WebComponent,
244
- disposer,
245
- mismatch,
246
- mfeSupportsRerendering,
247
- } = bundleInfo;
45
+ export const Loader = forwardRef<LoaderRef, LoaderProps<{}>>((props, ref) => {
46
+ const { basename, fallbackSrc, onRequestError, retries, src } = props;
248
47
 
249
- if (mismatch) {
250
- logService?.warning({
251
- category: 'MicroFrontends.DependenciesMismatch',
252
- message:
253
- 'Some of the package dependencies have incompatible versions.',
254
- data: {
255
- package: src,
256
- dependencies: JSON.stringify(mismatch),
257
- },
258
- });
259
- }
48
+ const bus = useEventBus();
49
+ const [logService, exposedDependencies = EXPOSED_DEPENDENCIES, eventBus = bus] =
50
+ useOptionalDependencies(Log, EXPOSED_DEPENDENCIES_TOKEN, EVENT_BUS_TOKEN);
51
+ const cancelation = useCancelation();
260
52
 
261
- await Promise.all(
262
- urls.js.map(url => {
263
- const script = Array.from(document.scripts).find(
264
- ({ src }) => src === url
265
- );
266
- if (!script) {
267
- return new Promise<void>(resolve => {
268
- const script = document.createElement('script');
53
+ const LoadingFallback = useMemo(
54
+ () => getFallback(props.loadingFallback, 'Loading...'),
55
+ [props.loadingFallback]
56
+ );
269
57
 
270
- script.crossOrigin = 'anonymous';
271
- script.onload = () => {
272
- script.dataset.webComponent = WebComponent;
273
- script.onload = null;
274
- resolve();
275
- };
276
- script.async = true;
277
- script.src = url;
58
+ const ErrorFallback = useMemo(
59
+ () => getFallback(props.errorFallback, 'Something went wrong'),
60
+ [props.errorFallback]
61
+ );
278
62
 
279
- document.body.append(script);
280
- });
281
- } else if (!script.dataset.webComponent) {
282
- return new Promise<void>(resolve => {
283
- const originalOnload = script.onload;
284
- script.onload = (...args) => {
285
- originalOnload?.call(script, ...args);
286
- script.onload = null;
287
- resolve();
288
- };
289
- });
290
- }
63
+ const Loader = useMemo(
64
+ () =>
65
+ lazyLoader({
66
+ basename,
67
+ cancelation,
68
+ exposedDependencies,
69
+ eventBus,
70
+ fallbackSrc,
71
+ logService,
72
+ onRequestError,
73
+ retries,
74
+ src,
75
+ LoadingFallback,
76
+ }),
77
+ [
78
+ basename,
79
+ cancelation,
80
+ exposedDependencies,
81
+ eventBus,
82
+ logService,
83
+ onRequestError,
84
+ fallbackSrc,
85
+ retries,
86
+ src,
87
+ LoadingFallback,
88
+ ]
89
+ );
291
90
 
292
- return Promise.resolve();
293
- })
294
- );
91
+ return (
92
+ <ErrorBoundary moduleName={src} fallback={ErrorFallback}>
93
+ <Suspense fallback={<LoadingFallback />}>
94
+ <Loader ref={ref} data={props.data ?? {}} className={props.className} />
95
+ </Suspense>
96
+ </ErrorBoundary>
97
+ );
98
+ });
295
99
 
296
- const handleRef = (webComponent: IWebComponent | null) => {
297
- webComponent?.provide({
298
- logService,
299
- historyManager,
300
- exposedDependencies,
301
- eventBus,
302
- basename,
303
- onReady: () => {
304
- setIsLoading(false);
305
- },
306
- onDispose: () => {
307
- disposer();
308
- },
309
- });
310
- };
100
+ type LazyLoaderProps = Omit<LoaderProps<any>, 'loadingFallback'> & {
101
+ cancelation: AbortController;
102
+ exposedDependencies?: ExposedDependencies;
103
+ eventBus: EventBus;
104
+ logService?: Log;
105
+ LoadingFallback: () => JSX.Element;
106
+ };
311
107
 
312
- return {
313
- default: function LazyComponent(props: DataProps) {
314
- const stringifiedData = useMemo(
315
- () => JSON.stringify(props.data),
316
- [props.data]
317
- );
108
+ function lazyLoader(props: LazyLoaderProps) {
109
+ const { cancelation, eventBus } = props;
318
110
 
319
- /*
320
- * Set key for rendering the web component so that if the data from the Host change,
321
- * and the MFE's version of startup doesn't support watching for data changes, then
322
- * the web component will be re-mounted whenever the data changes.
323
- * This is only used as a fallback when the MFE's version is older.
324
- */
325
- const key = mfeSupportsRerendering
326
- ? undefined
327
- : stringifiedData;
111
+ const LazyWebComponent = lazyWebComponent(props);
328
112
 
329
- /*
330
- * Convert camelCase data props to kebab-case data attributes, prepended with data-
331
- * Ex: fooBar -> data-foo-bar
332
- */
333
- const dataAttrs = useMemo(
334
- () =>
335
- Object.fromEntries(
336
- Object.entries(props.data ?? {}).map(
337
- ([key, value]) => [
338
- 'data-' +
339
- key.replace(
340
- /[A-Z]/g,
341
- letter =>
342
- '-' + letter.toLowerCase()
343
- ),
344
- value,
345
- ]
346
- )
347
- ),
348
- [props.data]
349
- );
113
+ return forwardRef<LoaderRef, LoaderDataProps>((props: LoaderDataProps, ref) => {
114
+ useImperativeHandle(ref, () => eventBus, []);
350
115
 
351
- return (
352
- <WebComponent
353
- key={key}
354
- ref={handleRef}
355
- class={props.className}
356
- {...(urls.css.length
357
- ? {
358
- 'style-urls': encodeURIComponent(
359
- JSON.stringify(urls.css)
360
- ),
361
- }
362
- : {})}
363
- /*
364
- * Continue to spread data as separate data attributes for backwards compatibility,
365
- * in case the Host has updated web-components, but the MFE has not
366
- */
367
- {...dataAttrs}
368
- // Stringify data object so that it can be passed as a data-attribute
369
- data-mfe-data={stringifiedData}
370
- />
371
- );
372
- },
373
- };
374
- }),
375
- [] // eslint-disable-line react-hooks/exhaustive-deps
376
- );
116
+ useEffect(() => {
117
+ return () => {
118
+ cancelation.abort();
119
+ };
120
+ }, []);
377
121
 
378
- return <Lazy data={props.data} className={props.className} />;
379
- });
122
+ return <LazyWebComponent data={props.data} className={props.className} />;
123
+ });
124
+ }
380
125
 
381
- return Component;
382
- }, [
126
+ function lazyWebComponent(props: LazyLoaderProps) {
127
+ const {
128
+ basename,
129
+ cancelation,
130
+ exposedDependencies,
131
+ eventBus,
132
+ fallbackSrc,
133
+ logService,
134
+ onRequestError,
135
+ retries,
136
+ src,
137
+ LoadingFallback,
138
+ } = props;
139
+
140
+ return lazy<(props: LoaderDataProps) => JSX.Element | null>(async () => {
141
+ let bundleInfo: BundleInfo;
142
+ try {
143
+ bundleInfo = await getBundleInfo(
383
144
  src,
384
145
  fallbackSrc,
385
146
  exposedDependencies,
147
+ cancelation.signal,
386
148
  retries,
387
- onRequestError,
388
- logService,
389
- historyManager,
390
- basename,
391
- eventBus,
392
- ]);
149
+ onRequestError
150
+ );
151
+ } catch (e) {
152
+ if (cancelation.signal.aborted) {
153
+ return {
154
+ default: () => null,
155
+ };
156
+ }
393
157
 
394
- const LoadingFallback = useMemo(
395
- () => () => {
396
- if (loadingFallback !== undefined) {
397
- return loadingFallback as ReactElement;
398
- }
158
+ throw e;
159
+ }
399
160
 
400
- return <h2>Loading...</h2>;
401
- },
402
- [loadingFallback]
403
- );
161
+ validateBundleInfo(bundleInfo, { logService, packageName: src });
404
162
 
405
- const ErrorFallback = useMemo(
406
- () => () => {
407
- if (errorFallback !== undefined) {
408
- return errorFallback as ReactElement;
409
- }
163
+ await loadScripts(bundleInfo);
410
164
 
411
- return <h2>Something went wrong</h2>;
412
- },
413
- [errorFallback]
414
- );
165
+ return {
166
+ default: webComponent({
167
+ basename,
168
+ bundleInfo,
169
+ eventBus,
170
+ exposedDependencies,
171
+ logService,
172
+ LoadingFallback,
173
+ }),
174
+ };
175
+ });
176
+ }
415
177
 
416
- return (
417
- <ErrorBoundary moduleName={src} fallback={ErrorFallback}>
418
- {isLoading && <LoadingFallback />}
419
- <Suspense fallback={null}>
420
- <Loader ref={ref} data={data} className={className} />
421
- </Suspense>
422
- </ErrorBoundary>
423
- );
424
- }
425
- );
178
+ function getFallback(node: FallbackNode | undefined, message: string): () => JSX.Element {
179
+ return () => (node !== undefined ? (node as ReactElement) : <h2>{message}</h2>);
180
+ }
package/src/register.tsx CHANGED
@@ -1,8 +1,6 @@
1
- import { FC, ComponentType } from 'react';
2
- import { render, unmountComponentAtNode } from 'react-dom';
3
-
4
- import { Log } from '@servicetitan/log-service';
1
+ import { FC, ComponentType, useEffect } from 'react';
5
2
  import { DefaultPortalContext, TablePopupPropsContext } from '@servicetitan/design-system';
3
+ import { Log } from '@servicetitan/log-service';
6
4
  import { Provider } from '@servicetitan/react-ioc';
7
5
 
8
6
  import {
@@ -14,11 +12,12 @@ import {
14
12
  IS_WEB_COMPONENT_TOKEN,
15
13
  RENDER_ROOT_TOKEN,
16
14
  } from './common';
17
- import { EventBus, EVENT_BUS_TOKEN } from './event-bus';
18
- import { HistoryManager } from './history-manager';
19
- import { WEB_COMPONENT_NAME } from './globals';
20
15
  import { MFEDataContext } from './contexts/mfe-data-context';
21
16
  import { MFEMetadataContext } from './contexts/mfe-metadata-context';
17
+ import { EventBus, EVENT_BUS_TOKEN } from './event-bus';
18
+ import { WEB_COMPONENT_NAME } from './globals';
19
+ import { HistoryManager } from './history-manager';
20
+ import { render } from './render';
22
21
 
23
22
  function createLink(url: string, onLoad: () => void) {
24
23
  const element = document.createElement('link');
@@ -34,7 +33,11 @@ function createLink(url: string, onLoad: () => void) {
34
33
 
35
34
  const Placeholder: FC = () => <div style={{ display: 'none' }} />;
36
35
 
37
- export function register(Component: ComponentType, light: boolean) {
36
+ export interface RegisterOptions {
37
+ legacyRoot?: boolean;
38
+ }
39
+
40
+ export function register(Component: ComponentType, light: boolean, options?: RegisterOptions) {
38
41
  class WebComponent extends HTMLElement implements IWebComponent {
39
42
  private root?: ShadowRoot;
40
43
  private portal?: ShadowRoot;
@@ -52,6 +55,7 @@ export function register(Component: ComponentType, light: boolean) {
52
55
  private basename?: string;
53
56
  private onReady?: () => void;
54
57
  private onDispose?: () => void;
58
+ private view?: ReturnType<typeof render>;
55
59
 
56
60
  connectedCallback() {
57
61
  /*
@@ -92,7 +96,7 @@ export function register(Component: ComponentType, light: boolean) {
92
96
  .appendChild(document.createElement('div'))
93
97
  .attachShadow({ mode: 'open' });
94
98
 
95
- render(<Placeholder />, this.root);
99
+ this.view = render(<Placeholder />, this.root, options);
96
100
 
97
101
  this.styles.elements.push(...this.attachStyles(this.root, styleUrls));
98
102
  this.styles.elements.push(...this.attachStyles(this.portal, styleUrls));
@@ -106,7 +110,7 @@ export function register(Component: ComponentType, light: boolean) {
106
110
  element.remove();
107
111
  }
108
112
 
109
- unmountComponentAtNode(this.root);
113
+ this.view?.unmount();
110
114
  document.body.removeChild(this.portal!.host);
111
115
  this.root = undefined;
112
116
  this.portal = undefined;
@@ -179,7 +183,7 @@ export function register(Component: ComponentType, light: boolean) {
179
183
  portalShadowRoot: this.portal!,
180
184
  };
181
185
 
182
- render(
186
+ this.view?.rerender(
183
187
  <Provider
184
188
  singletons={[
185
189
  { provide: IS_WEB_COMPONENT_TOKEN, useValue: true },
@@ -210,14 +214,15 @@ export function register(Component: ComponentType, light: boolean) {
210
214
  <MFEDataContext.Provider value={{ ...mfeData }}>
211
215
  <MFEMetadataContext.Provider value={metadata}>
212
216
  <Component {...mfeData} />
217
+ {this.onReady && (
218
+ <RenderCallback callback={this.onReady} />
219
+ )}
213
220
  </MFEMetadataContext.Provider>
214
221
  </MFEDataContext.Provider>
215
222
  </Provider>
216
223
  </TablePopupPropsContext.Provider>
217
224
  </DefaultPortalContext.Provider>
218
- </Provider>,
219
- this.root!,
220
- this.onReady
225
+ </Provider>
221
226
  );
222
227
  }
223
228
  };
@@ -231,3 +236,9 @@ export function register(Component: ComponentType, light: boolean) {
231
236
 
232
237
  window.customElements.define(WEB_COMPONENT_NAME, WebComponent);
233
238
  }
239
+
240
+ function RenderCallback({ callback }: { callback: () => void }) {
241
+ // eslint-disable-next-line react-hooks/exhaustive-deps
242
+ useEffect(() => callback(), []);
243
+ return null;
244
+ }