@servicetitan/web-components 36.3.1 → 37.0.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 (73) hide show
  1. package/dist/anvil1-providers.d.ts +6 -0
  2. package/dist/anvil1-providers.d.ts.map +1 -0
  3. package/dist/anvil1-providers.js +23 -0
  4. package/dist/anvil1-providers.js.map +1 -0
  5. package/dist/anvil2-providers.d.ts +5 -0
  6. package/dist/anvil2-providers.d.ts.map +1 -0
  7. package/dist/anvil2-providers.js +13 -0
  8. package/dist/anvil2-providers.js.map +1 -0
  9. package/dist/css-injector/index.d.ts +3 -0
  10. package/dist/css-injector/index.d.ts.map +1 -0
  11. package/dist/css-injector/index.js +4 -0
  12. package/dist/css-injector/index.js.map +1 -0
  13. package/dist/css-injector/mfe-style-registry.d.ts +14 -0
  14. package/dist/css-injector/mfe-style-registry.d.ts.map +1 -0
  15. package/dist/css-injector/mfe-style-registry.js +63 -0
  16. package/dist/css-injector/mfe-style-registry.js.map +1 -0
  17. package/dist/css-injector/with-css-injector.d.ts +6 -0
  18. package/dist/css-injector/with-css-injector.d.ts.map +1 -0
  19. package/dist/css-injector/with-css-injector.js +38 -0
  20. package/dist/css-injector/with-css-injector.js.map +1 -0
  21. package/dist/get-style-urls.d.ts +2 -0
  22. package/dist/get-style-urls.d.ts.map +1 -0
  23. package/dist/get-style-urls.js +21 -0
  24. package/dist/get-style-urls.js.map +1 -0
  25. package/dist/globals.d.ts +3 -3
  26. package/dist/globals.d.ts.map +1 -1
  27. package/dist/globals.js +19 -4
  28. package/dist/globals.js.map +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/register-headless.d.ts.map +1 -1
  34. package/dist/register-headless.js +1 -0
  35. package/dist/register-headless.js.map +1 -1
  36. package/dist/register.d.ts.map +1 -1
  37. package/dist/register.js +14 -55
  38. package/dist/register.js.map +1 -1
  39. package/dist/render.d.ts.map +1 -1
  40. package/dist/render.js +3 -11
  41. package/dist/render.js.map +1 -1
  42. package/dist/utils/get-bundle-info.d.ts.map +1 -1
  43. package/dist/utils/get-bundle-info.js +11 -4
  44. package/dist/utils/get-bundle-info.js.map +1 -1
  45. package/dist/utils/get-entry-points.d.ts +1 -0
  46. package/dist/utils/get-entry-points.d.ts.map +1 -1
  47. package/dist/utils/get-entry-points.js.map +1 -1
  48. package/dist/utils/get-ld-service-token.d.ts +2 -3
  49. package/dist/utils/get-ld-service-token.d.ts.map +1 -1
  50. package/dist/utils/get-ld-service-token.js +2 -7
  51. package/dist/utils/get-ld-service-token.js.map +1 -1
  52. package/package.json +36 -14
  53. package/src/__mocks__/register-anvil2-harness.tsx +49 -0
  54. package/src/__mocks__/test-component.tsx +9 -2
  55. package/src/__tests__/loader.test.tsx +32 -0
  56. package/src/__tests__/register-anvil2-optional.test.tsx +32 -0
  57. package/src/__tests__/register-anvil2.test.tsx +11 -64
  58. package/src/anvil1-providers.tsx +28 -0
  59. package/src/anvil2-providers.tsx +13 -0
  60. package/src/css-injector/__tests__/mfe-style-registry.test.ts +163 -0
  61. package/src/css-injector/__tests__/with-css-injector.test.tsx +128 -0
  62. package/src/css-injector/index.ts +2 -0
  63. package/src/css-injector/mfe-style-registry.ts +48 -0
  64. package/src/css-injector/with-css-injector.tsx +40 -0
  65. package/src/get-style-urls.ts +23 -0
  66. package/src/globals.ts +30 -10
  67. package/src/index.ts +1 -0
  68. package/src/register-headless.ts +1 -0
  69. package/src/register.tsx +15 -66
  70. package/src/render.ts +3 -14
  71. package/src/utils/get-bundle-info.ts +12 -5
  72. package/src/utils/get-entry-points.ts +1 -0
  73. package/src/utils/get-ld-service-token.ts +2 -10
@@ -0,0 +1,28 @@
1
+ import * as Anvil1 from '@servicetitan/design-system';
2
+ import { FC, PropsWithChildren, useCallback } from 'react';
3
+
4
+ export const Anvil1Providers: FC<{ root: ShadowRoot; portal: ShadowRoot } & PropsWithChildren> = ({
5
+ root,
6
+ portal,
7
+ children,
8
+ }) => {
9
+ const getTablePopupProps = useCallback(
10
+ (props: object) => ({
11
+ ...props,
12
+ appendTo: root as unknown as HTMLElement,
13
+ }),
14
+ [root]
15
+ );
16
+
17
+ if (Anvil1.DefaultPortalContext === undefined || Anvil1.TablePopupPropsContext === undefined) {
18
+ return children;
19
+ }
20
+
21
+ return (
22
+ <Anvil1.DefaultPortalContext.Provider value={portal as unknown as HTMLElement}>
23
+ <Anvil1.TablePopupPropsContext.Provider value={getTablePopupProps}>
24
+ {children}
25
+ </Anvil1.TablePopupPropsContext.Provider>
26
+ </Anvil1.DefaultPortalContext.Provider>
27
+ );
28
+ };
@@ -0,0 +1,13 @@
1
+ import * as Anvil2 from '@servicetitan/anvil2';
2
+ import { FC, PropsWithChildren } from 'react';
3
+
4
+ export const Anvil2Providers: FC<{ portalAnvilContainer: HTMLDivElement } & PropsWithChildren> = ({
5
+ portalAnvilContainer,
6
+ children,
7
+ }) => {
8
+ if (Anvil2.PortalProvider === undefined) {
9
+ return children;
10
+ }
11
+
12
+ return <Anvil2.PortalProvider root={portalAnvilContainer}>{children}</Anvil2.PortalProvider>;
13
+ };
@@ -0,0 +1,163 @@
1
+ import { MfeStyleRegistry } from '../mfe-style-registry';
2
+
3
+ beforeAll(() => {
4
+ if (!CSSStyleSheet.prototype.replaceSync) {
5
+ CSSStyleSheet.prototype.replaceSync = jest.fn();
6
+ }
7
+ });
8
+
9
+ describe('[web-components] MfeStyleRegistry', () => {
10
+ const createRoot = () => {
11
+ const root = document.createElement('div').attachShadow({ mode: 'open' });
12
+ root.adoptedStyleSheets = [];
13
+ return root;
14
+ };
15
+
16
+ beforeEach(() => {
17
+ delete globalThis.__mfeStyles__;
18
+ });
19
+
20
+ describe(MfeStyleRegistry.for.name, () => {
21
+ test('initializes globalThis.__mfeStyles__', () => {
22
+ MfeStyleRegistry.for('test-mfe');
23
+
24
+ expect(globalThis.__mfeStyles__).toBeDefined();
25
+ });
26
+
27
+ test('returns the same registry for the same component', () => {
28
+ const a = MfeStyleRegistry.for('test-mfe');
29
+ const b = MfeStyleRegistry.for('test-mfe');
30
+
31
+ expect(a).toBe(b);
32
+ });
33
+
34
+ test('returns different registries for different components', () => {
35
+ const a = MfeStyleRegistry.for('mfe-a');
36
+ const b = MfeStyleRegistry.for('mfe-b');
37
+
38
+ expect(a).not.toBe(b);
39
+ });
40
+ });
41
+
42
+ describe('injectSheet', () => {
43
+ const defaultCss = '.foo { color: red; }';
44
+ const defaultSheetId = 'sheet-1';
45
+
46
+ let registry: MfeStyleRegistry;
47
+
48
+ beforeEach(() => {
49
+ registry = MfeStyleRegistry.for('test-mfe');
50
+ });
51
+
52
+ const subject = (sheetId = defaultSheetId, css = defaultCss) =>
53
+ registry.injectSheet(sheetId, css);
54
+
55
+ test('creates a new CSSStyleSheet and stores it', () => {
56
+ subject();
57
+
58
+ const sheet = registry.sheets.get('sheet-1');
59
+ expect(sheet).toBeInstanceOf(CSSStyleSheet);
60
+ expect(sheet!.replaceSync).toHaveBeenCalledWith(defaultCss);
61
+ });
62
+
63
+ test('adopts new sheets into observed roots', () => {
64
+ const root = createRoot();
65
+ registry.observeRoot(root);
66
+
67
+ subject();
68
+
69
+ expect(root.adoptedStyleSheets).toContain(registry.sheets.get('sheet-1'));
70
+ });
71
+
72
+ describe('when sheet with the same ID already exists', () => {
73
+ beforeEach(() => {
74
+ subject();
75
+ });
76
+
77
+ test('updates existing sheet in place via replaceSync', () => {
78
+ const sheet = registry.sheets.get(defaultSheetId)!;
79
+
80
+ const newCSS = '.foo { color: blue; }';
81
+ subject(defaultSheetId, newCSS);
82
+
83
+ expect(registry.sheets.get(defaultSheetId)).toBe(sheet);
84
+ expect(sheet.replaceSync).toHaveBeenLastCalledWith(newCSS);
85
+ });
86
+ });
87
+ });
88
+
89
+ describe('observeRoot', () => {
90
+ let registry: MfeStyleRegistry;
91
+
92
+ beforeEach(() => {
93
+ registry = MfeStyleRegistry.for('test-mfe');
94
+ });
95
+
96
+ test('adds root to the roots set', () => {
97
+ const root = createRoot();
98
+
99
+ registry.observeRoot(root);
100
+
101
+ expect(registry.roots.has(root)).toBe(true);
102
+ });
103
+
104
+ describe('when root is already observed', () => {
105
+ let root: ShadowRoot;
106
+
107
+ beforeEach(() => {
108
+ registry.injectSheet('a', '.a {}');
109
+ root = createRoot();
110
+ registry.observeRoot(root);
111
+ });
112
+
113
+ test('does not duplicate sheets', () => {
114
+ registry.observeRoot(root);
115
+
116
+ expect(root.adoptedStyleSheets).toHaveLength(1);
117
+ });
118
+ });
119
+
120
+ test('adopts all existing sheets into the root', () => {
121
+ registry.injectSheet('a', '.a {}');
122
+ registry.injectSheet('b', '.b {}');
123
+ const root = createRoot();
124
+
125
+ registry.observeRoot(root);
126
+
127
+ expect(root.adoptedStyleSheets).toEqual([...registry.sheets.values()]);
128
+ });
129
+ });
130
+
131
+ describe('unobserveRoot', () => {
132
+ let registry: MfeStyleRegistry;
133
+ let root: ShadowRoot;
134
+
135
+ beforeEach(() => {
136
+ registry = MfeStyleRegistry.for('test-mfe');
137
+ root = createRoot();
138
+ const defaultSheetId = 'sheet-1';
139
+ registry.injectSheet(defaultSheetId, '.foo {}');
140
+ registry.observeRoot(root);
141
+ });
142
+
143
+ test('removes root from the roots set', () => {
144
+ registry.unobserveRoot(root);
145
+
146
+ expect(registry.roots.has(root)).toBe(false);
147
+ });
148
+
149
+ test('removes adopted sheets from the root', () => {
150
+ registry.unobserveRoot(root);
151
+
152
+ expect(root.adoptedStyleSheets).toHaveLength(0);
153
+ });
154
+
155
+ test('stops receiving newly adopted sheets', () => {
156
+ registry.unobserveRoot(root);
157
+
158
+ registry.injectSheet('sheet-2', '.bar {}');
159
+
160
+ expect(root.adoptedStyleSheets).toHaveLength(0);
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,128 @@
1
+ import { render } from '@testing-library/react';
2
+ import { FC } from 'react';
3
+ import { useMFEMetadataContext } from '../../contexts/mfe-metadata-context';
4
+ import { MfeStyleRegistry } from '../mfe-style-registry';
5
+ import { withCssInjector } from '../with-css-injector';
6
+
7
+ jest.mock('../../contexts/mfe-metadata-context');
8
+ jest.mock('../../globals', () => ({
9
+ // eslint-disable-next-line @typescript-eslint/naming-convention
10
+ WEB_COMPONENT_NAME: 'test-mfe',
11
+ }));
12
+ jest.mock('../mfe-style-registry');
13
+
14
+ describe(`[web-components] ${withCssInjector.name}`, () => {
15
+ const innerTestId = 'inner';
16
+ const innerLabel = 'foo';
17
+ const InnerComponent: FC<{ label: string }> = ({ label }) => (
18
+ <div data-testid={innerTestId}>{label}</div>
19
+ );
20
+ const WrappedComponent = withCssInjector(InnerComponent);
21
+
22
+ let registry: jest.Mocked<MfeStyleRegistry>;
23
+ let shadowRoot: ShadowRoot;
24
+ let portalShadowRoot: ShadowRoot;
25
+
26
+ beforeEach(() => {
27
+ jest.clearAllMocks();
28
+ delete globalThis.__mfeSelfHosted__;
29
+ registry = {
30
+ observeRoot: jest.fn(),
31
+ unobserveRoot: jest.fn(),
32
+ } as unknown as jest.Mocked<MfeStyleRegistry>;
33
+ jest.mocked(MfeStyleRegistry.for).mockReturnValue(registry);
34
+ shadowRoot = document.createElement('div').attachShadow({ mode: 'open' });
35
+ portalShadowRoot = document.createElement('span').attachShadow({ mode: 'open' });
36
+ jest.mocked(useMFEMetadataContext).mockImplementation(() => ({
37
+ shadowRoot,
38
+ portalShadowRoot,
39
+ }));
40
+ });
41
+
42
+ const subject = () => render(<WrappedComponent label={innerLabel} />);
43
+
44
+ test('renders the wrapped component with its props', () => {
45
+ const { getByTestId } = subject();
46
+
47
+ expect(getByTestId(innerTestId).textContent).toBe(innerLabel);
48
+ });
49
+
50
+ test('retrieves the registry for the component', () => {
51
+ subject();
52
+
53
+ expect(MfeStyleRegistry.for).toHaveBeenCalledWith('test-mfe');
54
+ });
55
+
56
+ test('observes shadow root', () => {
57
+ subject();
58
+
59
+ expect(registry.observeRoot).toHaveBeenCalledWith(shadowRoot);
60
+ });
61
+
62
+ test('observes portal shadow root', () => {
63
+ subject();
64
+
65
+ expect(registry.observeRoot).toHaveBeenCalledWith(portalShadowRoot);
66
+ });
67
+
68
+ describe('when self-hosted', () => {
69
+ beforeEach(() => {
70
+ globalThis.__mfeSelfHosted__ = true;
71
+ });
72
+
73
+ test('observes document', () => {
74
+ subject();
75
+
76
+ expect(registry.observeRoot).toHaveBeenCalledWith(document);
77
+ });
78
+
79
+ test('unobserves document on unmount', () => {
80
+ const { unmount } = subject();
81
+
82
+ unmount();
83
+
84
+ expect(registry.unobserveRoot).toHaveBeenCalledWith(document);
85
+ });
86
+ });
87
+
88
+ describe('when unmounted', () => {
89
+ test('unobserves shadow root', () => {
90
+ const { unmount } = subject();
91
+
92
+ unmount();
93
+
94
+ expect(registry.unobserveRoot).toHaveBeenCalledWith(shadowRoot);
95
+ });
96
+
97
+ test('unobserves portal shadow root', () => {
98
+ const { unmount } = subject();
99
+
100
+ unmount();
101
+
102
+ expect(registry.unobserveRoot).toHaveBeenCalledWith(portalShadowRoot);
103
+ });
104
+ });
105
+
106
+ describe('in production', () => {
107
+ const originalEnv = process.env.NODE_ENV;
108
+
109
+ beforeEach(() => {
110
+ process.env.NODE_ENV = 'production';
111
+ });
112
+
113
+ afterEach(() => {
114
+ process.env.NODE_ENV = originalEnv;
115
+ });
116
+
117
+ test('returns Component directly', () => {
118
+ expect.assertions(1);
119
+
120
+ jest.isolateModules(() => {
121
+ const { withCssInjector: freshWithCssInjector } = require('../with-css-injector');
122
+ const Dummy: FC = () => <div />;
123
+
124
+ expect(freshWithCssInjector(Dummy)).toBe(Dummy);
125
+ });
126
+ });
127
+ });
128
+ });
@@ -0,0 +1,2 @@
1
+ export { MfeStyleRegistry } from './mfe-style-registry';
2
+ export { withCssInjector } from './with-css-injector';
@@ -0,0 +1,48 @@
1
+ declare global {
2
+ // eslint-disable-next-line @typescript-eslint/naming-convention
3
+ var __mfeStyles__: Record<string, MfeStyleRegistry> | undefined;
4
+ }
5
+
6
+ type StyleRoot = ShadowRoot | Document;
7
+
8
+ export class MfeStyleRegistry {
9
+ readonly sheets = new Map<string, CSSStyleSheet>();
10
+ readonly roots = new Set<StyleRoot>();
11
+
12
+ // Per-component state stored on globalThis.__mfeStyles__ so it survives Vite HMR module reloads.
13
+ static for(componentName: string): MfeStyleRegistry {
14
+ globalThis.__mfeStyles__ ??= {};
15
+ globalThis.__mfeStyles__[componentName] ??= new MfeStyleRegistry();
16
+ return globalThis.__mfeStyles__[componentName];
17
+ }
18
+
19
+ injectSheet(sheetId: string, css: string) {
20
+ let sheet = this.sheets.get(sheetId);
21
+ if (sheet) {
22
+ sheet.replaceSync(css);
23
+ } else {
24
+ sheet = new CSSStyleSheet();
25
+ sheet.replaceSync(css);
26
+ this.sheets.set(sheetId, sheet);
27
+ for (const root of this.roots) {
28
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
29
+ }
30
+ }
31
+ }
32
+
33
+ observeRoot(root: StyleRoot) {
34
+ if (this.roots.has(root)) {
35
+ return;
36
+ }
37
+ this.roots.add(root);
38
+ root.adoptedStyleSheets ??= [];
39
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, ...this.sheets.values()];
40
+ }
41
+
42
+ unobserveRoot(root: StyleRoot) {
43
+ this.roots.delete(root);
44
+ const sheets = new Set(this.sheets.values());
45
+ root.adoptedStyleSheets ??= [];
46
+ root.adoptedStyleSheets = root.adoptedStyleSheets.filter(s => !sheets.has(s));
47
+ }
48
+ }
@@ -0,0 +1,40 @@
1
+ import { ComponentType, useLayoutEffect } from 'react';
2
+ import { useMFEMetadataContext } from '../contexts/mfe-metadata-context';
3
+ import { WEB_COMPONENT_NAME } from '../globals';
4
+ import { MfeStyleRegistry } from './mfe-style-registry';
5
+
6
+ declare global {
7
+ // eslint-disable-next-line @typescript-eslint/naming-convention
8
+ var __mfeSelfHosted__: boolean | undefined;
9
+ }
10
+
11
+ /*
12
+ * HOC wrapping the MFE's root component in register(). Adopts sheets from the registry into
13
+ * the MFE's shadow roots. Only active in development; eliminated by Vite in production builds.
14
+ */
15
+ export function withCssInjector<P extends object>(Component: ComponentType<P>): ComponentType<P> {
16
+ if (process.env.NODE_ENV === 'production') {
17
+ return Component;
18
+ }
19
+
20
+ return function CssInjectorWrapper(props: P) {
21
+ const { shadowRoot, portalShadowRoot } = useMFEMetadataContext();
22
+
23
+ useLayoutEffect(() => {
24
+ const registry = MfeStyleRegistry.for(WEB_COMPONENT_NAME);
25
+ const roots = [
26
+ shadowRoot,
27
+ portalShadowRoot,
28
+ ...(globalThis.__mfeSelfHosted__ ? [document] : []),
29
+ ];
30
+
31
+ roots.forEach(root => registry.observeRoot(root));
32
+
33
+ return () => {
34
+ roots.forEach(root => registry.unobserveRoot(root));
35
+ };
36
+ }, [shadowRoot, portalShadowRoot]);
37
+
38
+ return <Component {...props} />;
39
+ };
40
+ }
@@ -0,0 +1,23 @@
1
+ export function getStyleUrls(light: boolean, sharedDependenciesNames: string[], el: HTMLElement) {
2
+ function getCssUrl(dependencyName: string, cssFileRegEx: RegExp) {
3
+ if (light && sharedDependenciesNames.includes(dependencyName)) {
4
+ return Array.from(document.getElementsByTagName('link')).find(
5
+ ({ href }) => href && cssFileRegEx.test(href)
6
+ )?.href;
7
+ }
8
+ }
9
+
10
+ const designSystemUrl = getCssUrl(
11
+ '@servicetitan/design-system',
12
+ /design-system(\..+)?\.bundle\.css/
13
+ );
14
+ const anvil2CssUrl = getCssUrl('@servicetitan/anvil2', /anvil2(\..+)?\.bundle\.css/);
15
+
16
+ return [
17
+ ...(designSystemUrl ? [designSystemUrl] : []),
18
+ ...(anvil2CssUrl ? [anvil2CssUrl] : []),
19
+ ...(el.hasAttribute('style-urls')
20
+ ? JSON.parse(decodeURIComponent(el.getAttribute('style-urls')!))
21
+ : []),
22
+ ];
23
+ }
package/src/globals.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/naming-convention */
2
+ // eslint-disable-next-line spaced-comment
3
+ /// <reference types="vite/client" /> — provides types for import.meta.env
2
4
  import { ExposedDependencies } from '@servicetitan/startup-utils';
3
5
  import { ExposedInstanceDependencies } from './common';
4
6
 
@@ -8,6 +10,13 @@ declare global {
8
10
  export const WEB_COMPONENT_NAME: string | undefined;
9
11
  }
10
12
 
13
+ /*
14
+ * Safely access a value that may not exist. Returns undefined instead of
15
+ * throwing a ReferenceError. Used below to try global constants first
16
+ * (set by webpack DefinePlugin), then fall back to import.meta.env
17
+ * (set by Vite's define config). One of these will always be defined
18
+ * depending on which bundler built the code.
19
+ */
11
20
  function get<T>(fetch: () => T) {
12
21
  try {
13
22
  return fetch();
@@ -16,17 +25,28 @@ function get<T>(fetch: () => T) {
16
25
  }
17
26
  }
18
27
 
19
- const _EXPOSED_DEPENDENCIES = get(() => {
20
- return EXPOSED_DEPENDENCIES;
21
- });
28
+ const _EXPOSED_DEPENDENCIES =
29
+ get(() => {
30
+ return EXPOSED_DEPENDENCIES;
31
+ }) ??
32
+ get(() => {
33
+ return import.meta.env.EXPOSED_DEPENDENCIES;
34
+ });
22
35
 
23
- const _WEB_COMPONENT_NAME = get(() => {
24
- return WEB_COMPONENT_NAME;
25
- });
26
-
27
- const _EXPOSED_INSTANCE_DEPENDENCIES = get(() => {
28
- return EXPOSED_INSTANCE_DEPENDENCIES;
29
- });
36
+ const _WEB_COMPONENT_NAME =
37
+ get(() => {
38
+ return WEB_COMPONENT_NAME;
39
+ }) ??
40
+ get(() => {
41
+ return import.meta.env.WEB_COMPONENT_NAME;
42
+ });
43
+ const _EXPOSED_INSTANCE_DEPENDENCIES =
44
+ get(() => {
45
+ return EXPOSED_INSTANCE_DEPENDENCIES;
46
+ }) ??
47
+ get(() => {
48
+ return import.meta.env.EXPOSED_INSTANCE_DEPENDENCIES;
49
+ });
30
50
 
31
51
  export {
32
52
  _EXPOSED_DEPENDENCIES as EXPOSED_DEPENDENCIES,
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './common';
2
2
  export * from './contexts';
3
+ export * from './css-injector';
3
4
  export * from './event-bus';
4
5
  export * from './get-value-for-environment';
5
6
  export * from './headless-loader';
@@ -2,6 +2,7 @@ import type { LDService } from '@servicetitan/launchdarkly-service';
2
2
  import type { Log } from '@servicetitan/log-service';
3
3
  import type { Entries, IWebComponent } from './common';
4
4
  import type { EventBus } from './event-bus';
5
+ import { WEB_COMPONENT_NAME } from './globals';
5
6
  import { Serializable } from './types';
6
7
 
7
8
  type HeadlessCallbackArgs<TMfeData, TEventBus extends EventBus = EventBus> = Pick<
package/src/register.tsx CHANGED
@@ -3,6 +3,8 @@ import { Log } from '@servicetitan/log-service';
3
3
  import { Provider } from '@servicetitan/react-ioc';
4
4
  import { ExposedDependencies } from '@servicetitan/startup-utils';
5
5
  import { ComponentType, FC, ReactElement, useEffect } from 'react';
6
+ import { Anvil1Providers } from './anvil1-providers';
7
+ import { Anvil2Providers } from './anvil2-providers';
6
8
  import {
7
9
  BASENAME_TOKEN,
8
10
  Entries,
@@ -15,7 +17,9 @@ import {
15
17
  } from './common';
16
18
  import { MFEDataContext } from './contexts/mfe-data-context';
17
19
  import { MFEMetadataContext } from './contexts/mfe-metadata-context';
20
+ import { withCssInjector } from './css-injector';
18
21
  import { EVENT_BUS_TOKEN, EventBus } from './event-bus';
22
+ import { getStyleUrls } from './get-style-urls';
19
23
  import { WEB_COMPONENT_NAME } from './globals';
20
24
  import { HistoryManager } from './history-manager';
21
25
  import { render } from './render';
@@ -41,6 +45,8 @@ export interface RegisterOptions {
41
45
  }
42
46
 
43
47
  export function register(Component: ComponentType, light: boolean, options?: RegisterOptions) {
48
+ const WrappedComponent = withCssInjector(Component);
49
+
44
50
  class WebComponent extends HTMLElement implements IWebComponent {
45
51
  private root?: ShadowRoot;
46
52
  private portal?: ShadowRoot;
@@ -165,47 +171,14 @@ export function register(Component: ComponentType, light: boolean, options?: Reg
165
171
  return elements;
166
172
  };
167
173
 
168
- private withAnvil1Providers(children: ReactElement): ReactElement {
169
- try {
170
- /**
171
- * Note, the webpack.IgnorePlugin must be configured to ignore this dependency.
172
- * @see {@link file://./../../startup/src/webpack/configs/plugins/ignore-plugin/ignore-plugin.ts}
173
- */
174
- const { DefaultPortalContext, TablePopupPropsContext } =
175
- require('@servicetitan/design-system') as typeof import('@servicetitan/design-system');
176
- return (
177
- <DefaultPortalContext.Provider value={this.portal as unknown as HTMLElement}>
178
- <TablePopupPropsContext.Provider
179
- value={props => ({
180
- ...props,
181
- appendTo: this.root as unknown as HTMLElement,
182
- })}
183
- >
184
- {children}
185
- </TablePopupPropsContext.Provider>
186
- </DefaultPortalContext.Provider>
187
- );
188
- } catch {
189
- return children;
190
- }
191
- }
192
-
193
- private withAnvil2Providers(children: ReactElement): ReactElement {
194
- try {
195
- /**
196
- * Note, the webpack.IgnorePlugin must be configured to ignore this dependency.
197
- * @see {@link file://./../../startup/src/webpack/configs/plugins/ignore-plugin/ignore-plugin.ts}
198
- */
199
- const { PortalProvider } =
200
- require('@servicetitan/anvil2') as typeof import('@servicetitan/anvil2');
201
- return <PortalProvider root={this.portalAnvilContainer}>{children}</PortalProvider>;
202
- } catch {
203
- return children;
204
- }
205
- }
206
-
207
- private withProviders(children: ReactElement): ReactElement {
208
- return this.withAnvil2Providers(this.withAnvil1Providers(children));
174
+ private withProviders(children: ReactElement) {
175
+ return (
176
+ <Anvil2Providers portalAnvilContainer={this.portalAnvilContainer!}>
177
+ <Anvil1Providers root={this.root!} portal={this.portal!}>
178
+ {children}
179
+ </Anvil1Providers>
180
+ </Anvil2Providers>
181
+ );
209
182
  }
210
183
 
211
184
  private render = () => {
@@ -264,7 +237,7 @@ export function register(Component: ComponentType, light: boolean, options?: Reg
264
237
  >
265
238
  <MFEDataContext.Provider value={{ ...mfeData }}>
266
239
  <MFEMetadataContext.Provider value={metadata}>
267
- {this.withProviders(<Component {...mfeData} />)}
240
+ {this.withProviders(<WrappedComponent {...mfeData} />)}
268
241
  {this.onReady && <RenderCallback callback={this.onReady} />}
269
242
  </MFEMetadataContext.Provider>
270
243
  </MFEDataContext.Provider>
@@ -284,30 +257,6 @@ export function register(Component: ComponentType, light: boolean, options?: Reg
284
257
  window.customElements.define(WEB_COMPONENT_NAME, WebComponent);
285
258
  }
286
259
 
287
- function getStyleUrls(light: boolean, sharedDependenciesNames: string[], el: HTMLElement) {
288
- function getCssUrl(dependencyName: string, cssFileRegEx: RegExp) {
289
- if (light && sharedDependenciesNames.includes(dependencyName)) {
290
- return Array.from(document.getElementsByTagName('link')).find(
291
- ({ href }) => href && cssFileRegEx.test(href)
292
- )?.href;
293
- }
294
- }
295
-
296
- const designSystemUrl = getCssUrl(
297
- '@servicetitan/design-system',
298
- /design-system(\..+)?\.bundle\.css/
299
- );
300
- const anvil2CssUrl = getCssUrl('@servicetitan/anvil2', /anvil2(\..+)?\.bundle\.css/);
301
-
302
- return [
303
- ...(designSystemUrl ? [designSystemUrl] : []),
304
- ...(anvil2CssUrl ? [anvil2CssUrl] : []),
305
- ...(el.hasAttribute('style-urls')
306
- ? JSON.parse(decodeURIComponent(el.getAttribute('style-urls')!))
307
- : []),
308
- ];
309
- }
310
-
311
260
  function RenderCallback({ callback }: { callback: () => void }) {
312
261
  // eslint-disable-next-line react-hooks/exhaustive-deps
313
262
  useEffect(() => callback(), []);
package/src/render.ts CHANGED
@@ -1,18 +1,7 @@
1
1
  /* eslint-disable react/no-deprecated */
2
2
  import { ReactElement } from 'react';
3
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 {
14
- // ignore
15
- }
4
+ import * as ReactDOMClient from 'react-dom/client';
16
5
 
17
6
  interface RenderOptions {
18
7
  legacyRoot?: boolean;
@@ -28,11 +17,11 @@ export function render(
28
17
  container: Element | DocumentFragment,
29
18
  options?: RenderOptions
30
19
  ): RenderResult {
31
- if (typeof createRoot !== 'function' || options?.legacyRoot) {
20
+ if (typeof ReactDOMClient.createRoot !== 'function' || options?.legacyRoot) {
32
21
  return renderLegacyRoot(element, container);
33
22
  }
34
23
 
35
- const root = createRoot(container);
24
+ const root = ReactDOMClient.createRoot(container);
36
25
  root.render(element);
37
26
 
38
27
  return {