@servicetitan/web-components 36.0.0 → 36.1.1-beta.1
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/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 +38 -36
- 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/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/get-bundle-info.d.ts +1 -0
- package/dist/utils/get-bundle-info.d.ts.map +1 -1
- package/dist/utils/get-bundle-info.js +14 -7
- package/dist/utils/get-bundle-info.js.map +1 -1
- package/dist/utils/get-entry-points.d.ts +2 -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/dist/utils/load-scripts.d.ts +1 -1
- package/dist/utils/load-scripts.d.ts.map +1 -1
- package/dist/utils/load-scripts.js +6 -2
- package/dist/utils/load-scripts.js.map +1 -1
- package/dist/with-css-injector.d.ts +3 -0
- package/dist/with-css-injector.d.ts.map +1 -0
- package/dist/with-css-injector.js +52 -0
- package/dist/with-css-injector.js.map +1 -0
- package/package.json +21 -12
- package/src/__mocks__/register-anvil2-harness.tsx +49 -0
- package/src/__mocks__/test-component.tsx +9 -2
- package/src/__tests__/loader.test.tsx +12 -0
- package/src/__tests__/register-anvil2-optional.test.tsx +32 -0
- package/src/__tests__/register-anvil2.test.tsx +11 -64
- package/src/__tests__/with-css-injector.test.tsx +152 -0
- package/src/globals.ts +30 -10
- package/src/index.ts +1 -0
- package/src/register-headless.ts +1 -0
- package/src/register.tsx +48 -42
- package/src/render.ts +3 -14
- package/src/types.ts +1 -1
- package/src/utils/get-bundle-info.ts +14 -7
- package/src/utils/get-entry-points.ts +2 -0
- package/src/utils/get-ld-service-token.ts +2 -10
- package/src/utils/load-scripts.ts +7 -2
- package/src/with-css-injector.tsx +45 -0
package/src/register.tsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import * as Anvil2 from '@servicetitan/anvil2';
|
|
2
|
+
import * as Anvil1 from '@servicetitan/design-system';
|
|
1
3
|
import { type LDService } from '@servicetitan/launchdarkly-service';
|
|
2
4
|
import { Log } from '@servicetitan/log-service';
|
|
3
5
|
import { Provider } from '@servicetitan/react-ioc';
|
|
4
6
|
import { ExposedDependencies } from '@servicetitan/startup-utils';
|
|
5
|
-
import { ComponentType, FC, ReactElement, useEffect } from 'react';
|
|
7
|
+
import { ComponentType, FC, PropsWithChildren, ReactElement, useCallback, useEffect } from 'react';
|
|
6
8
|
import {
|
|
7
9
|
BASENAME_TOKEN,
|
|
8
10
|
Entries,
|
|
@@ -40,6 +42,43 @@ export interface RegisterOptions {
|
|
|
40
42
|
sharedDependenciesNames?: string[];
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
const Anvil1Providers: FC<{ root: ShadowRoot; portal: ShadowRoot } & PropsWithChildren> = ({
|
|
46
|
+
root,
|
|
47
|
+
portal,
|
|
48
|
+
children,
|
|
49
|
+
}) => {
|
|
50
|
+
const getTablePopupProps = useCallback(
|
|
51
|
+
(props: object) => ({
|
|
52
|
+
...props,
|
|
53
|
+
appendTo: root as unknown as HTMLElement,
|
|
54
|
+
}),
|
|
55
|
+
[root]
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (Anvil1.DefaultPortalContext === undefined || Anvil1.TablePopupPropsContext === undefined) {
|
|
59
|
+
return children;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Anvil1.DefaultPortalContext.Provider value={portal as unknown as HTMLElement}>
|
|
64
|
+
<Anvil1.TablePopupPropsContext.Provider value={getTablePopupProps}>
|
|
65
|
+
{children}
|
|
66
|
+
</Anvil1.TablePopupPropsContext.Provider>
|
|
67
|
+
</Anvil1.DefaultPortalContext.Provider>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const Anvil2Providers: FC<{ portalAnvilContainer: HTMLDivElement } & PropsWithChildren> = ({
|
|
72
|
+
portalAnvilContainer,
|
|
73
|
+
children,
|
|
74
|
+
}) => {
|
|
75
|
+
if (Anvil2.PortalProvider === undefined) {
|
|
76
|
+
return children;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return <Anvil2.PortalProvider root={portalAnvilContainer}>{children}</Anvil2.PortalProvider>;
|
|
80
|
+
};
|
|
81
|
+
|
|
43
82
|
export function register(Component: ComponentType, light: boolean, options?: RegisterOptions) {
|
|
44
83
|
class WebComponent extends HTMLElement implements IWebComponent {
|
|
45
84
|
private root?: ShadowRoot;
|
|
@@ -165,47 +204,14 @@ export function register(Component: ComponentType, light: boolean, options?: Reg
|
|
|
165
204
|
return elements;
|
|
166
205
|
};
|
|
167
206
|
|
|
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));
|
|
207
|
+
private withProviders(children: ReactElement) {
|
|
208
|
+
return (
|
|
209
|
+
<Anvil2Providers portalAnvilContainer={this.portalAnvilContainer!}>
|
|
210
|
+
<Anvil1Providers root={this.root!} portal={this.portal!}>
|
|
211
|
+
{children}
|
|
212
|
+
</Anvil1Providers>
|
|
213
|
+
</Anvil2Providers>
|
|
214
|
+
);
|
|
209
215
|
}
|
|
210
216
|
|
|
211
217
|
private render = () => {
|
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 {
|
package/src/types.ts
CHANGED
|
@@ -18,5 +18,5 @@ export interface Metadata {
|
|
|
18
18
|
bundledWith?: Record<string, string | undefined>;
|
|
19
19
|
sharedDependencies: Record<string, string>;
|
|
20
20
|
dependencies?: Record<string, string>;
|
|
21
|
-
entrypoints?: Record<string, { css: string[]; js: string[] }>;
|
|
21
|
+
entrypoints?: Record<string, { css: string[]; js: string[]; module?: true }>;
|
|
22
22
|
}
|
|
@@ -78,14 +78,13 @@ export async function getBundleInfo({
|
|
|
78
78
|
resolvedBundleType = embed ? 'light' : 'full';
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
const
|
|
82
|
-
`${exactPackageUrl}/dist/bundle/${resolvedBundleType}/${path}`;
|
|
81
|
+
const bundleBaseUrl = `${exactPackageUrl}/dist/bundle/${resolvedBundleType}`;
|
|
83
82
|
|
|
84
83
|
let entrypoints: EntryPoints;
|
|
85
84
|
if (metadata.entrypoints?.[resolvedBundleType]) {
|
|
86
85
|
entrypoints = metadata.entrypoints[resolvedBundleType];
|
|
87
86
|
} else {
|
|
88
|
-
entrypoints = await getEntrypoints(
|
|
87
|
+
entrypoints = await getEntrypoints(`${bundleBaseUrl}/entrypoints.json`, requestCache);
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
const disposer = () => {
|
|
@@ -97,10 +96,17 @@ export async function getBundleInfo({
|
|
|
97
96
|
}
|
|
98
97
|
};
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
let baseUrl = bundleBaseUrl;
|
|
100
|
+
if (entrypoints.port) {
|
|
101
|
+
const url = new URL(baseUrl);
|
|
102
|
+
url.port = String(entrypoints.port);
|
|
103
|
+
baseUrl = url.toString();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const urls: EntryPoints = {
|
|
107
|
+
css: entrypoints.css.map(path => `${baseUrl}/${path}`),
|
|
108
|
+
js: entrypoints.js.map(path => `${baseUrl}/${path}`),
|
|
109
|
+
};
|
|
104
110
|
|
|
105
111
|
const elementName = resolvedBundleType === 'headless' ? `headless-${name}` : name;
|
|
106
112
|
|
|
@@ -119,5 +125,6 @@ export async function getBundleInfo({
|
|
|
119
125
|
urls,
|
|
120
126
|
WebComponent: elementName,
|
|
121
127
|
version: metadata.version,
|
|
128
|
+
module: entrypoints.module,
|
|
122
129
|
};
|
|
123
130
|
}
|
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { SymbolToken } from '@servicetitan/react-ioc';
|
|
3
|
-
|
|
4
|
-
let token: SymbolToken<LDService>;
|
|
5
|
-
try {
|
|
6
|
-
token = require('@servicetitan/launchdarkly-service').LD_SERVICE_TOKEN;
|
|
7
|
-
} catch {
|
|
8
|
-
// ignore
|
|
9
|
-
}
|
|
1
|
+
import * as LDService from '@servicetitan/launchdarkly-service';
|
|
10
2
|
|
|
11
3
|
export function getLDServiceToken() {
|
|
12
|
-
return
|
|
4
|
+
return LDService.LD_SERVICE_TOKEN;
|
|
13
5
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BundleInfo } from './get-bundle-info';
|
|
2
2
|
|
|
3
|
-
export function loadScripts({ urls, WebComponent }: BundleInfo) {
|
|
3
|
+
export function loadScripts({ urls, WebComponent, module }: BundleInfo) {
|
|
4
4
|
return Promise.all(
|
|
5
5
|
urls.js.map(url => {
|
|
6
6
|
const script = Array.from(document.scripts).find(({ src }) => src === url);
|
|
@@ -8,13 +8,18 @@ export function loadScripts({ urls, WebComponent }: BundleInfo) {
|
|
|
8
8
|
return new Promise<void>(resolve => {
|
|
9
9
|
const script = document.createElement('script');
|
|
10
10
|
|
|
11
|
+
if (module) {
|
|
12
|
+
script.type = 'module';
|
|
13
|
+
} else {
|
|
14
|
+
script.async = true;
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
script.crossOrigin = 'anonymous';
|
|
12
18
|
script.onload = () => {
|
|
13
19
|
script.dataset.webComponent = WebComponent;
|
|
14
20
|
script.onload = null;
|
|
15
21
|
resolve();
|
|
16
22
|
};
|
|
17
|
-
script.async = true;
|
|
18
23
|
script.src = url;
|
|
19
24
|
|
|
20
25
|
document.body.append(script);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ComponentType, useEffect } from 'react';
|
|
2
|
+
import { useMFEMetadataContext } from './contexts/mfe-metadata-context';
|
|
3
|
+
import { WEB_COMPONENT_NAME } from './globals';
|
|
4
|
+
|
|
5
|
+
export function withCssInjector<P extends object>(Component: ComponentType<P>): ComponentType<P> {
|
|
6
|
+
return function CssInjectorWrapper(props: P) {
|
|
7
|
+
const { shadowRoot, portalShadowRoot } = useMFEMetadataContext();
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
window.__mfeStyles__ ??= {};
|
|
11
|
+
const mfeStyles = window.__mfeStyles__;
|
|
12
|
+
mfeStyles[WEB_COMPONENT_NAME] ??= Object.assign(new EventTarget(), {
|
|
13
|
+
sheets: new Map<string, CSSStyleSheet>(),
|
|
14
|
+
});
|
|
15
|
+
const emitter = mfeStyles[WEB_COMPONENT_NAME];
|
|
16
|
+
/*
|
|
17
|
+
* In self-hosted standalone dev mode (set by index.html), also adopt sheets
|
|
18
|
+
* into document so host-only CSS vars and fonts propagate to the page.
|
|
19
|
+
*/
|
|
20
|
+
const roots = [
|
|
21
|
+
shadowRoot,
|
|
22
|
+
portalShadowRoot,
|
|
23
|
+
...(window.__mfeSelfHosted__ ? [document] : []),
|
|
24
|
+
];
|
|
25
|
+
function adoptSheet(sheet: CSSStyleSheet) {
|
|
26
|
+
for (const root of roots) {
|
|
27
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const sheet of emitter.sheets.values()) {
|
|
32
|
+
adoptSheet(sheet);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleNewSheet = ({ detail: sheetId }: CustomEvent<string>) => {
|
|
36
|
+
adoptSheet(emitter.sheets.get(sheetId)!);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
emitter.addEventListener('new-sheet', handleNewSheet);
|
|
40
|
+
return () => emitter.removeEventListener('new-sheet', handleNewSheet);
|
|
41
|
+
}, [shadowRoot, portalShadowRoot]);
|
|
42
|
+
|
|
43
|
+
return <Component {...props} />;
|
|
44
|
+
};
|
|
45
|
+
}
|