/** * Create an HTML LINK element for a CSS resource that points to the given CSS URL. * @param href URL to the CSS file. * @param preload * @returns The newly created HTML LINK element (unattached to the document). */ export function createLinkElement(href: string, preload?: boolean) { const el = globalThis.document.createElement("link"); if (preload) { el.rel = "preload"; el.as = "style"; } else { el.rel = "stylesheet"; el.disabled = true; } el.href = href; return el; } let microApps: MicroApp[] = []; export enum MicroAppType { ESM = "ESM", SYSTEMJS = "SYSTEMJS", } export type AppMetadata = { // app name name: string; // app entry entry: string; cssEntry?: string; container: string; prefetch?: boolean; props?: any; type?: MicroAppType; }; type MicroApp = AppMetadata & { loadedApp?: any; loadedCss?: HTMLLinkElement; bootstrapped?: boolean; }; export function registerMicroApps(apps: AppMetadata[]) { // Each app only needs to be registered once const unregisteredApps: MicroApp[] = apps.filter( (app) => !microApps.some((registeredApp) => registeredApp.name === app.name) ); microApps = [...microApps, ...unregisteredApps]; unregisteredApps.forEach(async (app) => { if (app.prefetch) { app.loadedApp = app.type === MicroAppType.SYSTEMJS ? await System.import(app.entry) : await import(/* @vite-ignore */ app.entry); if (app.cssEntry) { document.head.appendChild(createLinkElement(app.cssEntry, true)); } } }); } export type Parcel = { unmount(): Promise; update?(customProps: ExtraProps): Promise; }; type ParcelProps = { domElement: HTMLElement; basename?: string; url?: string }; type LifeCycleFn = (config: ExtraProps & ParcelProps) => Promise; export type LifeCycles = { bootstrap: Array>; mount: Array>; unmount: Array>; update?: Array>; }; function trimUrlAppProps(appProps: ParcelProps) { if (appProps.basename && appProps.url?.startsWith(appProps.basename)) { appProps.url = appProps.url.replace(appProps.basename, ""); } return appProps as any; } async function mountParcel( parcelConfig: LifeCycles, parcelProps: ParcelProps & ExtraProps ): Promise> { const appProps = trimUrlAppProps(parcelProps); await Promise.all(parcelConfig.mount.map(async (fn) => await fn(appProps))); return { unmount: async () => { await Promise.all(parcelConfig.unmount.map(async (fn) => await fn(appProps))); }, update: async (props: ExtraProps) => { const appProps = trimUrlAppProps({ ...parcelProps, ...props }); await Promise.all(parcelConfig.update?.map(async (fn) => await fn(appProps)) || []); }, }; } // create a function to subscribe to when the app props change, and call the callback with the new props. Return a function to unsubscribe type AppProps = { userData?: any; env?: any; url?: string; theme?: string; }; let currentProps: AppProps = { env: { baseURL: "https://bvhttdl.ioc-cloud.com/api-ioc", tenant: "iocv3-bvh", }, }; const subscribers: Array<(props: AppProps) => void> = []; function subscribeToAppPropsChange(callback: (props: AppProps) => void) { const index = subscribers.length; subscribers.push(callback); // Call the callback immediately with the current props callback(currentProps); // Return an unsubscribe function return () => { subscribers.splice(index, 1); }; } function setAppProps(newProps: AppProps) { currentProps = { ...currentProps, ...newProps }; subscribers.forEach((callback) => callback(currentProps)); } function onQueryParamChange() { const url = new URL(location.href); const moduleInputsQuery = url.searchParams.get("moduleInputs"); if (moduleInputsQuery) { const parsedInput = JSON.parse(moduleInputsQuery); if (parsedInput?.accessToken) { setAppProps({ userData: { accessToken: parsedInput.accessToken }, theme: parsedInput?.colorScheme ?? "light", }); } } } onQueryParamChange(); (function (history) { const pushState = history.pushState; history.pushState = function (...args) { // Call your custom function here onQueryParamChange(); return pushState.apply(history, args); }; })(window.history); export async function mountMicroApp(appName: string, mountProps?: any): Promise { const app = microApps.find((app) => app.name === appName); if (!app) return null; mountProps = { ...currentProps, ...mountProps, }; const { container, props, ...appConfig } = app; let parcelConfig: LifeCycles; const loadedApp = app.loadedApp ?? (app.type === MicroAppType.SYSTEMJS ? await System.import(appConfig.entry) : await import(/* @vite-ignore */ appConfig.entry)); if (appConfig.cssEntry) { const cssLifeCycles = { async bootstrap() { if (!app.loadedCss) { app.loadedCss = createLinkElement(appConfig.cssEntry!); app.loadedCss.disabled = true; document.head.appendChild(app.loadedCss); } else app.loadedCss.disabled = false; }, async mount() { app.loadedCss!.disabled = false; }, async unmount() { app.loadedCss!.disabled = true; }, }; parcelConfig = { bootstrap: [cssLifeCycles.bootstrap, loadedApp.bootstrap], mount: [cssLifeCycles.mount, loadedApp.mount], unmount: [cssLifeCycles.unmount, loadedApp.unmount], update: [loadedApp.update], }; } else parcelConfig = { bootstrap: [loadedApp.bootstrap], mount: [loadedApp.mount], unmount: [loadedApp.unmount], update: [loadedApp.update], }; if (!app.bootstrapped) { await Promise.all( parcelConfig.bootstrap.map( async (fn) => await fn({ ...props, ...mountProps, }) ) ); app.bootstrapped = true; } const microApp = await mountParcel(parcelConfig, { container, domElement: document.getElementById(container), ...props, ...mountProps, }); const unsubscribe = subscribeToAppPropsChange((appProps) => { // This code will execute whenever ANY of signal1, signal2, or signal3 changes. microApp.update?.({ ...props, ...mountProps, ...appProps, }); }); return { ...microApp, unmount(): Promise { unsubscribe(); return microApp.unmount(); }, }; }