@servicetitan/web-components 36.1.1-beta.1 → 36.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 (47) hide show
  1. package/dist/globals.d.ts +3 -3
  2. package/dist/globals.d.ts.map +1 -1
  3. package/dist/globals.js +4 -19
  4. package/dist/globals.js.map +1 -1
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +0 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/register-headless.d.ts.map +1 -1
  10. package/dist/register-headless.js +0 -1
  11. package/dist/register-headless.js.map +1 -1
  12. package/dist/register.d.ts.map +1 -1
  13. package/dist/register.js +36 -38
  14. package/dist/register.js.map +1 -1
  15. package/dist/render.d.ts.map +1 -1
  16. package/dist/render.js +11 -3
  17. package/dist/render.js.map +1 -1
  18. package/dist/utils/get-bundle-info.d.ts.map +1 -1
  19. package/dist/utils/get-bundle-info.js +4 -10
  20. package/dist/utils/get-bundle-info.js.map +1 -1
  21. package/dist/utils/get-entry-points.d.ts +0 -1
  22. package/dist/utils/get-entry-points.d.ts.map +1 -1
  23. package/dist/utils/get-entry-points.js.map +1 -1
  24. package/dist/utils/get-ld-service-token.d.ts +3 -2
  25. package/dist/utils/get-ld-service-token.d.ts.map +1 -1
  26. package/dist/utils/get-ld-service-token.js +7 -2
  27. package/dist/utils/get-ld-service-token.js.map +1 -1
  28. package/package.json +12 -21
  29. package/src/__mocks__/test-component.tsx +2 -9
  30. package/src/__tests__/event-bus-types.test.ts +2 -1
  31. package/src/__tests__/register-anvil2.test.tsx +64 -11
  32. package/src/globals.ts +10 -30
  33. package/src/index.ts +0 -1
  34. package/src/register-headless.ts +0 -1
  35. package/src/register.tsx +42 -48
  36. package/src/render.ts +14 -3
  37. package/src/utils/get-bundle-info.ts +5 -11
  38. package/src/utils/get-entry-points.ts +0 -1
  39. package/src/utils/get-ld-service-token.ts +10 -2
  40. package/dist/with-css-injector.d.ts +0 -3
  41. package/dist/with-css-injector.d.ts.map +0 -1
  42. package/dist/with-css-injector.js +0 -52
  43. package/dist/with-css-injector.js.map +0 -1
  44. package/src/__mocks__/register-anvil2-harness.tsx +0 -49
  45. package/src/__tests__/register-anvil2-optional.test.tsx +0 -32
  46. package/src/__tests__/with-css-injector.test.tsx +0 -152
  47. package/src/with-css-injector.tsx +0 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/web-components",
3
- "version": "36.1.1-beta.1",
3
+ "version": "36.2.0",
4
4
  "description": "Framework to build, register, and load Microfrontends into host applications",
5
5
  "homepage": "https://docs.st.dev/docs/frontend/uikit/web-components",
6
6
  "repository": {
@@ -10,34 +10,24 @@
10
10
  },
11
11
  "main": "./dist/index.js",
12
12
  "typings": "./dist/index.d.ts",
13
- "exports": {
14
- ".": {
15
- "types": "./dist/index.d.ts",
16
- "default": "./dist/index.js"
17
- },
18
- "./register-headless": {
19
- "types": "./dist/register-headless.d.ts",
20
- "default": "./dist/register-headless.js"
21
- }
22
- },
23
13
  "sideEffects": false,
24
14
  "files": [
25
15
  "dist",
26
16
  "src"
27
17
  ],
28
18
  "dependencies": {
29
- "@servicetitan/error-boundary": "36.1.1-beta.1",
30
- "@servicetitan/startup-utils": "36.1.1-beta.1",
19
+ "@servicetitan/error-boundary": "36.2.0",
20
+ "@servicetitan/startup-utils": "36.2.0",
31
21
  "lodash.get": "~4.4.2",
32
22
  "mitt": "~3.0.1",
33
23
  "semver": "~7.7.4"
34
24
  },
35
25
  "devDependencies": {
36
- "@servicetitan/anvil2": "^2.8.0",
26
+ "@servicetitan/anvil2": "^3.0.4",
37
27
  "@servicetitan/design-system": "~14.5.3",
38
- "@servicetitan/launchdarkly-service": "36.1.1-beta.1",
39
- "@servicetitan/log-service": "36.1.1-beta.1",
40
- "@servicetitan/react-ioc": "36.1.1-beta.1",
28
+ "@servicetitan/launchdarkly-service": "36.2.0",
29
+ "@servicetitan/log-service": "36.2.0",
30
+ "@servicetitan/react-ioc": "36.2.0",
41
31
  "@testing-library/dom": "^10.4.1",
42
32
  "@testing-library/jest-dom": "^6.9.1",
43
33
  "@testing-library/react": "^16.3.2",
@@ -55,9 +45,9 @@
55
45
  "peerDependencies": {
56
46
  "@servicetitan/anvil2": ">=1.5.0",
57
47
  "@servicetitan/design-system": ">=14.5.3",
58
- "@servicetitan/launchdarkly-service": "36.1.1-beta.1",
59
- "@servicetitan/log-service": "36.1.1-beta.1",
60
- "@servicetitan/react-ioc": "36.1.1-beta.1",
48
+ "@servicetitan/launchdarkly-service": "36.2.0",
49
+ "@servicetitan/log-service": "36.2.0",
50
+ "@servicetitan/react-ioc": "36.2.0",
61
51
  "history": "~4.10.1",
62
52
  "react": ">=17.0.2",
63
53
  "react-dom": ">=17.0.2"
@@ -78,5 +68,6 @@
78
68
  },
79
69
  "cli": {
80
70
  "webpack": false
81
- }
71
+ },
72
+ "gitHead": "87254b2caf60b20eb46c33b40ffb4dd05ad3e0f7"
82
73
  }
@@ -1,20 +1,13 @@
1
1
  import { TablePopupPropsContext } from '@servicetitan/design-system';
2
- import { createContext, FC, useContext } from 'react';
2
+ import { FC, useContext } from 'react';
3
3
  import { useMFEDataContext, useMFEMetadataContext } from '../contexts';
4
4
 
5
5
  jest.mock('@progress/kendo-licensing', () => ({ validatePackage: jest.fn() }));
6
6
 
7
- const FallbackContext = createContext(undefined);
8
-
9
7
  export const MockComponent: FC<Record<string, any>> = props => {
10
8
  const data = useMFEDataContext();
11
9
  const metadata = useMFEMetadataContext();
12
- /*
13
- * TablePopupPropsContext is undefined when a test mocks
14
- * @servicetitan/design-system as an empty object. Fall back to
15
- * a no-op context so the component renders without it.
16
- */
17
- const popupProps = useContext(TablePopupPropsContext ?? FallbackContext);
10
+ const popupProps = useContext(TablePopupPropsContext);
18
11
  const appendTo = popupProps?.({}).appendTo;
19
12
 
20
13
  return (
@@ -27,7 +27,8 @@ describe(`[web-components] ${EventBus.name}`, () => {
27
27
  describe('EventBus<T>', () => {
28
28
  test('rejects invalid data type', () => {
29
29
  // @ts-expect-error Types of property 'foo' are incompatible.
30
- eventBus = EventBus<{ foo: () => void }>;
30
+ const eventBus = EventBus<{ foo: () => void }>;
31
+ expect(eventBus).toBeDefined();
31
32
  });
32
33
  });
33
34
 
@@ -1,11 +1,7 @@
1
1
  import '@testing-library/jest-dom';
2
- import { within } from '@testing-library/react';
2
+ import { render, within } from '@testing-library/react';
3
3
  import { Fragment } from 'react';
4
- import { expectStylesInDom } from '../__mocks__';
5
- import {
6
- createRegisterHarness,
7
- optionalLibraryWrappers,
8
- } from '../__mocks__/register-anvil2-harness';
4
+ import { expectStylesInDom, MockComponent, registerComponent } from '../__mocks__';
9
5
 
10
6
  function mockComponent(name: string) {
11
7
  return {
@@ -41,7 +37,30 @@ jest.mock('@servicetitan/design-system', () => ({
41
37
  }));
42
38
 
43
39
  describe('[web-components] register', () => {
44
- const { Page, subject } = createRegisterHarness('mock-anvil2-app');
40
+ const componentName = 'mock-anvil2-app';
41
+
42
+ class Page {
43
+ static get componentShadowRoot() {
44
+ return document.querySelector(componentName)!.shadowRoot!;
45
+ }
46
+
47
+ static get portalShadowRoot() {
48
+ return document.querySelector('body')!.lastElementChild!.shadowRoot!;
49
+ }
50
+ }
51
+
52
+ beforeAll(() => {
53
+ registerComponent(MockComponent, componentName, {
54
+ sharedDependenciesNames: ['@servicetitan/anvil2'],
55
+ });
56
+ });
57
+
58
+ const subject = () => {
59
+ jest.spyOn(console, 'error').mockImplementation(jest.fn()); // suppress "ReactDOM.render deprecated" error
60
+ const Component: any = componentName;
61
+ const handleRef = (webComponent: any) => webComponent?.provide({ foo: 'bar' }); // triggers rendering
62
+ return render(<Component ref={handleRef} />);
63
+ };
45
64
 
46
65
  describe('when Anvil2 bundle is present in the DOM', () => {
47
66
  let cssLink: HTMLLinkElement;
@@ -71,10 +90,44 @@ describe('[web-components] register', () => {
71
90
  });
72
91
  });
73
92
 
74
- test.each(Object.values(optionalLibraryWrappers).flat())('wraps component within %s', name => {
75
- subject();
93
+ function itWrapsComponentWithin(name: string, expected: boolean = true) {
94
+ test(`${expected ? 'wraps' : 'does not wrap'} component within ${name}`, () => {
95
+ subject();
96
+
97
+ const element = within(Page.componentShadowRoot as any).queryByText(`<${name}>`);
98
+ if (expected) {
99
+ expect(element).toBeInTheDocument();
100
+ } else {
101
+ expect(element).not.toBeInTheDocument();
102
+ }
103
+ });
104
+ }
105
+
106
+ ['PortalProvider', 'DefaultPortalContext.Provider', 'TablePopupPropsContext.Provider'].forEach(
107
+ name => itWrapsComponentWithin(name)
108
+ );
109
+
110
+ describe('when @servicetitan/anvil2 is not present', () => {
111
+ beforeAll(() => {
112
+ jest.resetModules();
113
+ jest.mock('@servicetitan/anvil2', () => {
114
+ throw new Error('Cannot find module');
115
+ });
116
+ });
117
+
118
+ itWrapsComponentWithin('PortalProvider', false);
119
+ });
120
+
121
+ describe('when @servicetitan/design-system is not present', () => {
122
+ beforeAll(() => {
123
+ jest.resetModules();
124
+ jest.mock('@servicetitan/design-system', () => {
125
+ throw new Error('Cannot find module');
126
+ });
127
+ });
76
128
 
77
- const element = within(Page.componentShadowRoot as any).queryByText(`<${name}>`);
78
- expect(element).toBeInTheDocument();
129
+ ['DefaultPortalContext.Provider', 'TablePopupPropsContext.Provider'].forEach(name =>
130
+ itWrapsComponentWithin(name, false)
131
+ );
79
132
  });
80
133
  });
package/src/globals.ts CHANGED
@@ -1,6 +1,4 @@
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
4
2
  import { ExposedDependencies } from '@servicetitan/startup-utils';
5
3
  import { ExposedInstanceDependencies } from './common';
6
4
 
@@ -10,13 +8,6 @@ declare global {
10
8
  export const WEB_COMPONENT_NAME: string | undefined;
11
9
  }
12
10
 
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
- */
20
11
  function get<T>(fetch: () => T) {
21
12
  try {
22
13
  return fetch();
@@ -25,28 +16,17 @@ function get<T>(fetch: () => T) {
25
16
  }
26
17
  }
27
18
 
28
- const _EXPOSED_DEPENDENCIES =
29
- get(() => {
30
- return EXPOSED_DEPENDENCIES;
31
- }) ??
32
- get(() => {
33
- return import.meta.env.EXPOSED_DEPENDENCIES;
34
- });
19
+ const _EXPOSED_DEPENDENCIES = get(() => {
20
+ return EXPOSED_DEPENDENCIES;
21
+ });
35
22
 
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
- });
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
+ });
50
30
 
51
31
  export {
52
32
  _EXPOSED_DEPENDENCIES as EXPOSED_DEPENDENCIES,
package/src/index.ts CHANGED
@@ -10,4 +10,3 @@ export * from './register-headless';
10
10
  export * from './use-prefetch';
11
11
 
12
12
  export type { JSON, JSONPrimitive, Serializable } from './types';
13
- export { withCssInjector } from './with-css-injector';
@@ -2,7 +2,6 @@ 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';
6
5
  import { Serializable } from './types';
7
6
 
8
7
  type HeadlessCallbackArgs<TMfeData, TEventBus extends EventBus = EventBus> = Pick<
package/src/register.tsx CHANGED
@@ -1,10 +1,8 @@
1
- import * as Anvil2 from '@servicetitan/anvil2';
2
- import * as Anvil1 from '@servicetitan/design-system';
3
1
  import { type LDService } from '@servicetitan/launchdarkly-service';
4
2
  import { Log } from '@servicetitan/log-service';
5
3
  import { Provider } from '@servicetitan/react-ioc';
6
4
  import { ExposedDependencies } from '@servicetitan/startup-utils';
7
- import { ComponentType, FC, PropsWithChildren, ReactElement, useCallback, useEffect } from 'react';
5
+ import { ComponentType, FC, ReactElement, useEffect } from 'react';
8
6
  import {
9
7
  BASENAME_TOKEN,
10
8
  Entries,
@@ -42,43 +40,6 @@ export interface RegisterOptions {
42
40
  sharedDependenciesNames?: string[];
43
41
  }
44
42
 
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
-
82
43
  export function register(Component: ComponentType, light: boolean, options?: RegisterOptions) {
83
44
  class WebComponent extends HTMLElement implements IWebComponent {
84
45
  private root?: ShadowRoot;
@@ -204,14 +165,47 @@ export function register(Component: ComponentType, light: boolean, options?: Reg
204
165
  return elements;
205
166
  };
206
167
 
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
- );
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));
215
209
  }
216
210
 
217
211
  private render = () => {
package/src/render.ts CHANGED
@@ -1,7 +1,18 @@
1
1
  /* eslint-disable react/no-deprecated */
2
2
  import { ReactElement } from 'react';
3
3
  import ReactDOM from 'react-dom';
4
- import * as ReactDOMClient from 'react-dom/client';
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
+ }
5
16
 
6
17
  interface RenderOptions {
7
18
  legacyRoot?: boolean;
@@ -17,11 +28,11 @@ export function render(
17
28
  container: Element | DocumentFragment,
18
29
  options?: RenderOptions
19
30
  ): RenderResult {
20
- if (typeof ReactDOMClient.createRoot !== 'function' || options?.legacyRoot) {
31
+ if (typeof createRoot !== 'function' || options?.legacyRoot) {
21
32
  return renderLegacyRoot(element, container);
22
33
  }
23
34
 
24
- const root = ReactDOMClient.createRoot(container);
35
+ const root = createRoot(container);
25
36
  root.render(element);
26
37
 
27
38
  return {
@@ -78,13 +78,14 @@ export async function getBundleInfo({
78
78
  resolvedBundleType = embed ? 'light' : 'full';
79
79
  }
80
80
 
81
- const bundleBaseUrl = `${exactPackageUrl}/dist/bundle/${resolvedBundleType}`;
81
+ const withBaseUrl = (path: string) =>
82
+ `${exactPackageUrl}/dist/bundle/${resolvedBundleType}/${path}`;
82
83
 
83
84
  let entrypoints: EntryPoints;
84
85
  if (metadata.entrypoints?.[resolvedBundleType]) {
85
86
  entrypoints = metadata.entrypoints[resolvedBundleType];
86
87
  } else {
87
- entrypoints = await getEntrypoints(`${bundleBaseUrl}/entrypoints.json`, requestCache);
88
+ entrypoints = await getEntrypoints(withBaseUrl('entrypoints.json'), requestCache);
88
89
  }
89
90
 
90
91
  const disposer = () => {
@@ -96,16 +97,9 @@ export async function getBundleInfo({
96
97
  }
97
98
  };
98
99
 
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
100
  const urls: EntryPoints = {
107
- css: entrypoints.css.map(path => `${baseUrl}/${path}`),
108
- js: entrypoints.js.map(path => `${baseUrl}/${path}`),
101
+ css: entrypoints.css.map(withBaseUrl),
102
+ js: entrypoints.js.map(withBaseUrl),
109
103
  };
110
104
 
111
105
  const elementName = resolvedBundleType === 'headless' ? `headless-${name}` : name;
@@ -4,7 +4,6 @@ export interface EntryPoints {
4
4
  css: string[];
5
5
  js: string[];
6
6
  module?: true;
7
- port?: number;
8
7
  }
9
8
 
10
9
  export async function getEntrypoints(
@@ -1,5 +1,13 @@
1
- import * as LDService from '@servicetitan/launchdarkly-service';
1
+ import { type LDService } from '@servicetitan/launchdarkly-service';
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
+ }
2
10
 
3
11
  export function getLDServiceToken() {
4
- return LDService.LD_SERVICE_TOKEN;
12
+ return token;
5
13
  }
@@ -1,3 +0,0 @@
1
- import { ComponentType } from 'react';
2
- export declare function withCssInjector<P extends object>(Component: ComponentType<P>): ComponentType<P>;
3
- //# sourceMappingURL=with-css-injector.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"with-css-injector.d.ts","sourceRoot":"","sources":["../src/with-css-injector.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAa,MAAM,OAAO,CAAC;AAIjD,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAwC/F"}
@@ -1,52 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect } from 'react';
3
- import { useMFEMetadataContext } from './contexts/mfe-metadata-context';
4
- import { WEB_COMPONENT_NAME } from './globals';
5
- export function withCssInjector(Component) {
6
- return function CssInjectorWrapper(props) {
7
- const { shadowRoot, portalShadowRoot } = useMFEMetadataContext();
8
- useEffect(()=>{
9
- var _window, ___mfeStyles__, _mfeStyles, _WEB_COMPONENT_NAME, _;
10
- (___mfeStyles__ = (_window = window).__mfeStyles__) !== null && ___mfeStyles__ !== void 0 ? ___mfeStyles__ : _window.__mfeStyles__ = {};
11
- const mfeStyles = window.__mfeStyles__;
12
- (_ = (_mfeStyles = mfeStyles)[_WEB_COMPONENT_NAME = WEB_COMPONENT_NAME]) !== null && _ !== void 0 ? _ : _mfeStyles[_WEB_COMPONENT_NAME] = Object.assign(new EventTarget(), {
13
- sheets: new Map()
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
- */ const roots = [
20
- shadowRoot,
21
- portalShadowRoot,
22
- ...window.__mfeSelfHosted__ ? [
23
- document
24
- ] : []
25
- ];
26
- function adoptSheet(sheet) {
27
- for (const root of roots){
28
- root.adoptedStyleSheets = [
29
- ...root.adoptedStyleSheets,
30
- sheet
31
- ];
32
- }
33
- }
34
- for (const sheet of emitter.sheets.values()){
35
- adoptSheet(sheet);
36
- }
37
- const handleNewSheet = ({ detail: sheetId })=>{
38
- adoptSheet(emitter.sheets.get(sheetId));
39
- };
40
- emitter.addEventListener('new-sheet', handleNewSheet);
41
- return ()=>emitter.removeEventListener('new-sheet', handleNewSheet);
42
- }, [
43
- shadowRoot,
44
- portalShadowRoot
45
- ]);
46
- return /*#__PURE__*/ _jsx(Component, {
47
- ...props
48
- });
49
- };
50
- }
51
-
52
- //# sourceMappingURL=with-css-injector.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/with-css-injector.tsx"],"sourcesContent":["import { ComponentType, useEffect } from 'react';\nimport { useMFEMetadataContext } from './contexts/mfe-metadata-context';\nimport { WEB_COMPONENT_NAME } from './globals';\n\nexport function withCssInjector<P extends object>(Component: ComponentType<P>): ComponentType<P> {\n return function CssInjectorWrapper(props: P) {\n const { shadowRoot, portalShadowRoot } = useMFEMetadataContext();\n\n useEffect(() => {\n window.__mfeStyles__ ??= {};\n const mfeStyles = window.__mfeStyles__;\n mfeStyles[WEB_COMPONENT_NAME] ??= Object.assign(new EventTarget(), {\n sheets: new Map<string, CSSStyleSheet>(),\n });\n const emitter = mfeStyles[WEB_COMPONENT_NAME];\n /*\n * In self-hosted standalone dev mode (set by index.html), also adopt sheets\n * into document so host-only CSS vars and fonts propagate to the page.\n */\n const roots = [\n shadowRoot,\n portalShadowRoot,\n ...(window.__mfeSelfHosted__ ? [document] : []),\n ];\n function adoptSheet(sheet: CSSStyleSheet) {\n for (const root of roots) {\n root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];\n }\n }\n\n for (const sheet of emitter.sheets.values()) {\n adoptSheet(sheet);\n }\n\n const handleNewSheet = ({ detail: sheetId }: CustomEvent<string>) => {\n adoptSheet(emitter.sheets.get(sheetId)!);\n };\n\n emitter.addEventListener('new-sheet', handleNewSheet);\n return () => emitter.removeEventListener('new-sheet', handleNewSheet);\n }, [shadowRoot, portalShadowRoot]);\n\n return <Component {...props} />;\n };\n}\n"],"names":["useEffect","useMFEMetadataContext","WEB_COMPONENT_NAME","withCssInjector","Component","CssInjectorWrapper","props","shadowRoot","portalShadowRoot","window","mfeStyles","__mfeStyles__","Object","assign","EventTarget","sheets","Map","emitter","roots","__mfeSelfHosted__","document","adoptSheet","sheet","root","adoptedStyleSheets","values","handleNewSheet","detail","sheetId","get","addEventListener","removeEventListener"],"mappings":";AAAA,SAAwBA,SAAS,QAAQ,QAAQ;AACjD,SAASC,qBAAqB,QAAQ,kCAAkC;AACxE,SAASC,kBAAkB,QAAQ,YAAY;AAE/C,OAAO,SAASC,gBAAkCC,SAA2B;IACzE,OAAO,SAASC,mBAAmBC,KAAQ;QACvC,MAAM,EAAEC,UAAU,EAAEC,gBAAgB,EAAE,GAAGP;QAEzCD,UAAU;gBACNS,SAAAA,gBAEAC,YAAUR,qBAAVQ;aAFAD,iBAAAA,CAAAA,UAAAA,QAAOE,aAAa,cAApBF,4BAAAA,iBAAAA,QAAOE,aAAa,GAAK,CAAC;YAC1B,MAAMD,YAAYD,OAAOE,aAAa;aACtCD,IAAAA,CAAAA,aAAAA,UAAS,CAACR,sBAAAA,mBAAmB,cAA7BQ,eAAAA,IAAAA,UAAS,CAACR,oBAAmB,GAAKU,OAAOC,MAAM,CAAC,IAAIC,eAAe;gBAC/DC,QAAQ,IAAIC;YAChB;YACA,MAAMC,UAAUP,SAAS,CAACR,mBAAmB;YAC7C;;;aAGC,GACD,MAAMgB,QAAQ;gBACVX;gBACAC;mBACIC,OAAOU,iBAAiB,GAAG;oBAACC;iBAAS,GAAG,EAAE;aACjD;YACD,SAASC,WAAWC,KAAoB;gBACpC,KAAK,MAAMC,QAAQL,MAAO;oBACtBK,KAAKC,kBAAkB,GAAG;2BAAID,KAAKC,kBAAkB;wBAAEF;qBAAM;gBACjE;YACJ;YAEA,KAAK,MAAMA,SAASL,QAAQF,MAAM,CAACU,MAAM,GAAI;gBACzCJ,WAAWC;YACf;YAEA,MAAMI,iBAAiB,CAAC,EAAEC,QAAQC,OAAO,EAAuB;gBAC5DP,WAAWJ,QAAQF,MAAM,CAACc,GAAG,CAACD;YAClC;YAEAX,QAAQa,gBAAgB,CAAC,aAAaJ;YACtC,OAAO,IAAMT,QAAQc,mBAAmB,CAAC,aAAaL;QAC1D,GAAG;YAACnB;YAAYC;SAAiB;QAEjC,qBAAO,KAACJ;YAAW,GAAGE,KAAK;;IAC/B;AACJ"}
@@ -1,49 +0,0 @@
1
- import { render } from '@testing-library/react';
2
- import { MockComponent, registerComponent } from './index';
3
-
4
- /* Wrapper components injected by each optional library. */
5
- export const optionalLibraryWrappers: Record<string, string[]> = {
6
- '@servicetitan/anvil2': ['PortalProvider'],
7
- '@servicetitan/design-system': [
8
- 'DefaultPortalContext.Provider',
9
- 'TablePopupPropsContext.Provider',
10
- ],
11
- };
12
-
13
- export interface RegisterHarness {
14
- Page: {
15
- readonly componentShadowRoot: ShadowRoot;
16
- readonly portalShadowRoot: ShadowRoot;
17
- };
18
- subject: () => ReturnType<typeof render>;
19
- }
20
-
21
- export function createRegisterHarness(
22
- componentName: string,
23
- options?: Parameters<typeof registerComponent>[2]
24
- ): RegisterHarness {
25
- const Page = {
26
- get componentShadowRoot() {
27
- return document.querySelector(componentName)!.shadowRoot!;
28
- },
29
- get portalShadowRoot() {
30
- return document.querySelector('body')!.lastElementChild!.shadowRoot!;
31
- },
32
- };
33
-
34
- beforeAll(() => {
35
- registerComponent(MockComponent, componentName, {
36
- sharedDependenciesNames: ['@servicetitan/anvil2'],
37
- ...options,
38
- });
39
- });
40
-
41
- const subject = () => {
42
- jest.spyOn(console, 'error').mockImplementation(jest.fn()); // suppress "ReactDOM.render deprecated" error
43
- const Component: any = componentName;
44
- const handleRef = (wc: any) => wc?.provide({ foo: 'bar' }); // triggers rendering
45
- return render(<Component ref={handleRef} />);
46
- };
47
-
48
- return { Page, subject };
49
- }
@@ -1,32 +0,0 @@
1
- /*
2
- * Split from register-anvil2.test.tsx because register.tsx changed from
3
- * dynamic require() to static `import * as` for Vite/ESM compatibility.
4
- * The previous approach used jest.resetModules() mid-file to re-mock
5
- * modules between tests, but static imports are resolved once at module
6
- * load time and aren't affected by resetModules. Separate files give
7
- * each test suite its own jest.mock declarations.
8
- */
9
- import '@testing-library/jest-dom';
10
- import { within } from '@testing-library/react';
11
- import {
12
- createRegisterHarness,
13
- optionalLibraryWrappers,
14
- } from '../__mocks__/register-anvil2-harness';
15
-
16
- jest.mock('@servicetitan/anvil2', () => ({}));
17
- jest.mock('@servicetitan/design-system', () => ({}));
18
-
19
- describe('[web-components] register', () => {
20
- const { Page, subject } = createRegisterHarness('mock-no-optional-deps');
21
-
22
- for (const [library, wrappers] of Object.entries(optionalLibraryWrappers)) {
23
- describe(`when ${library} is not present`, () => {
24
- test.each(wrappers)('does not wrap component within %s', name => {
25
- subject();
26
-
27
- const element = within(Page.componentShadowRoot as any).queryByText(`<${name}>`);
28
- expect(element).not.toBeInTheDocument();
29
- });
30
- });
31
- }
32
- });