@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.
- package/dist/anvil1-providers.d.ts +6 -0
- package/dist/anvil1-providers.d.ts.map +1 -0
- package/dist/anvil1-providers.js +23 -0
- package/dist/anvil1-providers.js.map +1 -0
- package/dist/anvil2-providers.d.ts +5 -0
- package/dist/anvil2-providers.d.ts.map +1 -0
- package/dist/anvil2-providers.js +13 -0
- package/dist/anvil2-providers.js.map +1 -0
- package/dist/css-injector/index.d.ts +3 -0
- package/dist/css-injector/index.d.ts.map +1 -0
- package/dist/css-injector/index.js +4 -0
- package/dist/css-injector/index.js.map +1 -0
- package/dist/css-injector/mfe-style-registry.d.ts +14 -0
- package/dist/css-injector/mfe-style-registry.d.ts.map +1 -0
- package/dist/css-injector/mfe-style-registry.js +63 -0
- package/dist/css-injector/mfe-style-registry.js.map +1 -0
- package/dist/css-injector/with-css-injector.d.ts +6 -0
- package/dist/css-injector/with-css-injector.d.ts.map +1 -0
- package/dist/css-injector/with-css-injector.js +38 -0
- package/dist/css-injector/with-css-injector.js.map +1 -0
- package/dist/get-style-urls.d.ts +2 -0
- package/dist/get-style-urls.d.ts.map +1 -0
- package/dist/get-style-urls.js +21 -0
- package/dist/get-style-urls.js.map +1 -0
- package/dist/globals.d.ts +3 -3
- package/dist/globals.d.ts.map +1 -1
- package/dist/globals.js +19 -4
- package/dist/globals.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/register-headless.d.ts.map +1 -1
- package/dist/register-headless.js +1 -0
- package/dist/register-headless.js.map +1 -1
- package/dist/register.d.ts.map +1 -1
- package/dist/register.js +14 -55
- package/dist/register.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +3 -11
- package/dist/render.js.map +1 -1
- package/dist/utils/get-bundle-info.d.ts.map +1 -1
- package/dist/utils/get-bundle-info.js +11 -4
- package/dist/utils/get-bundle-info.js.map +1 -1
- package/dist/utils/get-entry-points.d.ts +1 -0
- package/dist/utils/get-entry-points.d.ts.map +1 -1
- package/dist/utils/get-entry-points.js.map +1 -1
- package/dist/utils/get-ld-service-token.d.ts +2 -3
- package/dist/utils/get-ld-service-token.d.ts.map +1 -1
- package/dist/utils/get-ld-service-token.js +2 -7
- package/dist/utils/get-ld-service-token.js.map +1 -1
- package/package.json +36 -14
- package/src/__mocks__/register-anvil2-harness.tsx +49 -0
- package/src/__mocks__/test-component.tsx +9 -2
- package/src/__tests__/loader.test.tsx +32 -0
- package/src/__tests__/register-anvil2-optional.test.tsx +32 -0
- package/src/__tests__/register-anvil2.test.tsx +11 -64
- package/src/anvil1-providers.tsx +28 -0
- package/src/anvil2-providers.tsx +13 -0
- package/src/css-injector/__tests__/mfe-style-registry.test.ts +163 -0
- package/src/css-injector/__tests__/with-css-injector.test.tsx +128 -0
- package/src/css-injector/index.ts +2 -0
- package/src/css-injector/mfe-style-registry.ts +48 -0
- package/src/css-injector/with-css-injector.tsx +40 -0
- package/src/get-style-urls.ts +23 -0
- package/src/globals.ts +30 -10
- package/src/index.ts +1 -0
- package/src/register-headless.ts +1 -0
- package/src/register.tsx +15 -66
- package/src/render.ts +3 -14
- package/src/utils/get-bundle-info.ts +12 -5
- package/src/utils/get-entry-points.ts +1 -0
- 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,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 =
|
|
20
|
-
|
|
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 =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
package/src/register-headless.ts
CHANGED
|
@@ -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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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(<
|
|
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
|
|
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 {
|