import { ExposedDependencies, getVersionMismatches } from '@servicetitan/startup-utils'; import get from 'lodash.get'; import semver from 'semver'; import { ExposedInstanceDependencies, getJson, GetJsonError } from '../common'; import type { Metadata } from '../types'; import { getCacheSetting } from './get-cache-setting'; import { getCachedMetadata } from './get-cached-metadata'; import { EntryPoints, getEntrypoints } from './get-entry-points'; import { getMFEHasCompatibleLaunchDarklyVersion } from './get-mfe-has-compatible-launchdarkly-version'; import { getMFEHasUniqueChunkNamespace } from './get-mfe-has-unique-chunk-namespace'; import { getMFESupportsRerendering } from './get-mfe-supports-rerendering'; import { getMFESupportsVersionedLDClient } from './get-mfe-supports-versioned-ld-client'; export type BundleInfo = Awaited>; interface GetBundleInfoProps { cache: boolean | number; exposedDependencies?: ExposedDependencies; exposedInstanceDependencies?: ExposedInstanceDependencies; fallbackPackageUrl?: string; isHeadless?: boolean; mainPackageUrl: string; onRequestError?: (error: GetJsonError) => void; retries?: number; signal?: AbortSignal; } export async function getBundleInfo({ cache, mainPackageUrl, fallbackPackageUrl, exposedDependencies = {}, exposedInstanceDependencies = {}, isHeadless, signal, retries, onRequestError, }: GetBundleInfoProps) { const { cacheHit, data: metadata, url: packageUrl, } = await getCachedMetadata({ cache, fallbackPackageUrl, mainPackageUrl, onRequestError, retries, signal, }); const { bundledWith, name, sharedDependencies } = metadata; const exactPackageUrl = packageUrl.replace('/dist/metadata.json', ''); const requestCache = getCacheSetting(exactPackageUrl); let dependencies: NonNullable; if (metadata.dependencies) { dependencies = metadata.dependencies; } else { const { data: packageJson } = await getJson(`${exactPackageUrl}/package.json`, { cache: requestCache, retries, signal, }); dependencies = packageJson.dependencies; } let mismatch: ReturnType; let resolvedBundleType: 'headless' | 'light' | 'full'; if (isHeadless) { resolvedBundleType = 'headless'; } else { mismatch = getVersionMismatches(exposedDependencies, dependencies, sharedDependencies); const startupVersion = bundledWith?.['@servicetitan/startup']; const supportLightBundles = !!startupVersion && semver.gte(startupVersion, '17.0.0'); const embed = !mismatch && supportLightBundles; resolvedBundleType = embed ? 'light' : 'full'; } const bundleBaseUrl = `${exactPackageUrl}/dist/bundle/${resolvedBundleType}`; let entrypoints: EntryPoints; if (metadata.entrypoints?.[resolvedBundleType]) { entrypoints = metadata.entrypoints[resolvedBundleType]; } else { entrypoints = await getEntrypoints(`${bundleBaseUrl}/entrypoints.json`, requestCache); } const disposer = () => { if (resolvedBundleType === 'light') { const path = Object.values(sharedDependencies).shift(); if (path) { get(window, path).getStorage().clear(name); } } }; let baseUrl = bundleBaseUrl; // Per-MFE dev-server port only happens during local development if (process.env.NODE_ENV !== 'production' && entrypoints.port) { const url = new URL(baseUrl); url.port = String(entrypoints.port); baseUrl = url.toString(); } const urls: EntryPoints = { css: entrypoints.css.map(path => `${baseUrl}/${path}`), js: entrypoints.js.map(path => `${baseUrl}/${path}`), }; const elementName = resolvedBundleType === 'headless' ? `headless-${name}` : name; return { bundledWith, cacheHit, disposer, mfeSupportsRerendering: getMFESupportsRerendering(bundledWith, dependencies), mfeHasUniqueChunkNamespace: getMFEHasUniqueChunkNamespace(bundledWith), mfeHasCompatibleLaunchDarklyVersion: getMFEHasCompatibleLaunchDarklyVersion( bundledWith, exposedInstanceDependencies ), mfeSupportsVersionedLDClient: getMFESupportsVersionedLDClient(bundledWith), mismatch, resolvedBundleType, urls, WebComponent: elementName, version: metadata.version, module: entrypoints.module, }; }