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));
}
}