import { type LDService } from '@servicetitan/launchdarkly-service'; import { Log } from '@servicetitan/log-service'; import { useOptionalDependencies } from '@servicetitan/react-ioc'; import { ExposedDependencies } from '@servicetitan/startup-utils'; import { Fragment, useMemo, useState } from 'react'; import { ExposedInstanceDependencies, IWebComponent } from '../common'; import { MFEContextData } from '../contexts/mfe-data-context'; import { EventBus } from '../event-bus'; import { HistoryManager } from '../history-manager'; import { LoaderDataProps } from '../loader'; import { BundleInfo } from './get-bundle-info'; import { getElementName } from './get-element-name'; interface WebComponentProps { basename?: string; bundleInfo: BundleInfo; eventBus: EventBus; exposedDependencies?: ExposedDependencies; exposedInstanceDependencies?: ExposedInstanceDependencies; ldService?: LDService; logService?: Log; } export function webComponent(props: WebComponentProps) { const { basename, bundleInfo, eventBus, exposedDependencies, ldService, logService } = props; const { WebComponent, resolvedBundleType, disposer, mfeSupportsRerendering, version } = bundleInfo; const elementName = getElementName(bundleInfo); const exposedInstanceDependencies = ensureLDVersion( props.exposedInstanceDependencies, bundleInfo.bundledWith?.['launchdarkly-js-client-sdk'] ); return ({ LoadingFallback, ...innerProps }: LoaderDataProps) => { const [isLoading, setIsLoading] = useState(true); const [historyManager] = useOptionalDependencies(HistoryManager); const handleRef = (webComponent: IWebComponent | null) => { webComponent?.provide({ ldService, logService, historyManager, exposedDependencies, exposedInstanceDependencies, eventBus, basename, onReady: () => { setIsLoading(false); }, onDispose: () => { disposer(); }, }); }; // Stringify data object so that it can be passed as a data-attribute const stringifiedData = useMemo(() => JSON.stringify(innerProps.data), [innerProps.data]); /* * Set key for rendering the web component so that if the data from the Host change, * and the MFE's version of startup doesn't support watching for data changes, then * the web component will be re-mounted whenever the data changes. * This is only used as a fallback when the MFE's version is older. */ const key = mfeSupportsRerendering ? undefined : stringifiedData; const dataAttrs = useMemo(() => composeDataAttributes(innerProps.data), [innerProps.data]); const styleUrls = getStyleUrls(bundleInfo); return ( {isLoading && } ); }; } /* * Convert camelCase data props to kebab-case data attributes, prepended with data- * Ex: fooBar -> data-foo-bar */ function composeDataAttributes(data: MFEContextData<{}> | undefined = {}) { return Object.fromEntries( Object.entries(data).map(([key, value]) => [ 'data-' + key.replace(/[A-Z]/g, letter => '-' + letter.toLowerCase()), value, ]) ); } function ensureLDVersion( exposedInstanceDependencies: ExposedInstanceDependencies = {}, version?: string ) { if (version && !exposedInstanceDependencies.launchDarkly?.ldService) { return { ...exposedInstanceDependencies, launchDarkly: { version } }; } return exposedInstanceDependencies; } function getStyleUrls({ urls }: Pick) { if (urls.css.length) { return encodeURIComponent(JSON.stringify(urls.css)); } }