import { getPerformanceDebug } from './performanceDebugInfo' import { AnySlotKey, AppHost, EntryPoint, PrivateShell, SlotKey, StatisticsMemoization, Trace } from '../API' import _ from 'lodash' import { hot } from '../hot' import { AppHostServicesProvider } from '../appHostServices' import { AnyExtensionSlot } from '../extensionSlot' interface PerformanceDebugParams { options: AppHost['options'] trace: Trace[] memoizedArr: StatisticsMemoization[] } interface SetupDebugInfoParams { readyAPIs: Set host: AppHost & AppHostServicesProvider uniqueShellNames: Set extensionSlots: Map addedShells: Map shellInstallers: WeakMap performance: PerformanceDebugParams getUnreadyEntryPoints(): EntryPoint[] getOwnSlotKey(key: SlotKey): SlotKey getAPI: AppHost['getAPI'] } function mapApiToEntryPoint(allPackages: EntryPoint[]) { const apiToEntryPoint = new Map() _.forEach(allPackages, (entryPoint: EntryPoint) => { _.forEach(entryPoint.declareAPIs ? entryPoint.declareAPIs() : [], dependency => { apiToEntryPoint.set(dependency.name, entryPoint) }) }) return apiToEntryPoint } /** * a function that returns all the entry points in the system with their declared APIs and dependencies */ const getAllEntryPoints = () => { return [ ...globalThis.repluggableAppDebug.utils.unReadyEntryPoints(), ...[...globalThis.repluggableAppDebug.addedShells].map(([_, shell]) => shell.entryPoint) ] } /** * this function is used to get the root unready API in case there are too many to understand. * for example if you have 200 unready entry points, running this function will give you the first unready API that * will unblock the rest of the entry point (note that there might be more than one) * * this function basically takes the first unready entry point, get its dependencies and iterates over them to find an API that is not ready * at this point it follows the same process recursively until it reaced the target API. */ const getRootUnreadyAPI = (host: AppHost) => { return () => { // get all unready entry points const allEntryPoints = getAllEntryPoints() const unReadyAPIsArray = [] // get the depdenencies of the first unready entry point let dependenciesOfUnreadyEntryPoint = allEntryPoints?.[0]?.getDependencyAPIs?.() while (dependenciesOfUnreadyEntryPoint?.length) { const currentAPI = dependenciesOfUnreadyEntryPoint.pop() if (!currentAPI) { continue } // try to get the API from this host, we are looking for an API that is not ready try { const api = host.getAPI(currentAPI as SlotKey) if (api) { continue } } catch (e) { unReadyAPIsArray.push(currentAPI) // we found an API that is unready, lets find which entry point declares it const declarer = allEntryPoints.find(entryPointData => entryPointData.declareAPIs?.().some(api => currentAPI?.name === api.name) ) dependenciesOfUnreadyEntryPoint = declarer?.getDependencyAPIs?.() } } return unReadyAPIsArray.reverse()[0] } } export type DependencyTree = { entryPoint: string deps: Array<{ api: string; subtree: DependencyTree | null }> } const traceAPIDependency = (entryPointName: string, apiName: string, entryPoints: EntryPoint[]): DependencyTree | null => { const apiToDeclarer = mapApiToEntryPoint(entryPoints) const entryPointByName = new Map(entryPoints.map(ep => [ep.name, ep])) if (!entryPointByName.has(entryPointName)) { return null } const onPath = new Set() const build = (epName: string): DependencyTree | null => { if (onPath.has(epName)) { return null } onPath.add(epName) const deps: DependencyTree['deps'] = [] const epDeps = entryPointByName.get(epName)?.getDependencyAPIs?.() ?? [] for (const dep of epDeps) { if (dep.name === apiName) { deps.push({ api: dep.name, subtree: null }) } else { const declarer = apiToDeclarer.get(dep.name) const subtree = declarer ? build(declarer.name) : null if (subtree) { deps.push({ api: dep.name, subtree }) } } } onPath.delete(epName) return deps.length > 0 ? { entryPoint: epName, deps } : null } return build(entryPointName) } const visualizeDependencyTree = (tree: DependencyTree | null): string => { if (!tree) { return '(no dependency paths)' } const lines: string[] = [tree.entryPoint] const render = (node: DependencyTree, prefix: string): void => { node.deps.forEach((edge, i) => { const isLast = i === node.deps.length - 1 const connector = isLast ? '└─ ' : '├─ ' const nextPrefix = prefix + (isLast ? ' ' : '│ ') const declaredBy = edge.subtree ? ` (declared by ${edge.subtree.entryPoint})` : '' lines.push(`${prefix}${connector}${edge.api}${declaredBy}`) if (edge.subtree) { render(edge.subtree, nextPrefix) } }) } render(tree, '') return lines.join('\n') } const getAPIOrEntryPointsDependencies = ( apisOrEntryPointsNames: string[], entryPoints: EntryPoint[] ): { entryPoints: EntryPoint[]; apis: AnySlotKey[] } => { const apiToEntryPoint = mapApiToEntryPoint(entryPoints) const loadedEntryPoints = new Set() const packagesList: EntryPoint[] = [] const allDependencies = new Set() const apisOrEntryPointsSet = new Set(apisOrEntryPointsNames) const entryPointsQueue: EntryPoint[] = entryPoints.filter(x => apisOrEntryPointsSet.has(x.name)) apisOrEntryPointsNames.forEach(x => { const ep = apiToEntryPoint.get(x) ep && entryPointsQueue.push(ep) }) while (entryPointsQueue.length) { const currEntryPoint = entryPointsQueue.shift() if (!currEntryPoint || loadedEntryPoints.has(currEntryPoint.name)) { continue } loadedEntryPoints.add(currEntryPoint.name) packagesList.push(currEntryPoint) const dependencies = currEntryPoint.getDependencyAPIs ? currEntryPoint.getDependencyAPIs() : [] dependencies.forEach(x => allDependencies.add(x)) const dependencyEntryPoints = dependencies.map((API: AnySlotKey) => apiToEntryPoint.get(API.name)) entryPointsQueue.push(..._.compact(dependencyEntryPoints)) } return { entryPoints: packagesList, apis: [...allDependencies] } } export function setupDebugInfo({ host, uniqueShellNames, readyAPIs, getAPI, getOwnSlotKey, getUnreadyEntryPoints, extensionSlots, addedShells, shellInstallers, performance: { options, trace, memoizedArr } }: SetupDebugInfoParams) { const utils = { apis: () => { return Array.from(readyAPIs).map((apiKey: AnySlotKey) => { return { key: apiKey, impl: () => getAPI(apiKey) } }) }, getRootUnreadyAPI: getRootUnreadyAPI(host), unReadyEntryPoints: (): EntryPoint[] => getUnreadyEntryPoints(), whyEntryPointUnready: (name: string) => { const unreadyEntryPoint = _.find( utils.unReadyEntryPoints(), (entryPoint: EntryPoint) => entryPoint.name.toLowerCase() === name.toLowerCase() ) const dependencies = _.invoke(unreadyEntryPoint, 'getDependencyAPIs') const unreadyDependencies = _.filter(dependencies, key => !readyAPIs.has(getOwnSlotKey(key))) if (!_.isEmpty(unreadyDependencies)) { const unreadyDependenciesNames = _(unreadyDependencies).map('name').join(',') console.log(`There are unready dependencies for ${name}: ${unreadyDependenciesNames}`) } }, findAPI: (name: string) => { return _.filter(utils.apis(), (api: any) => api.key.name.toLowerCase().indexOf(name.toLowerCase()) !== -1) }, getAPIOrEntryPointsDependencies: ( apisOrEntryPointsNames: string[], entryPoints = [...addedShells.values()].map(x => x.entryPoint) ) => getAPIOrEntryPointsDependencies(apisOrEntryPointsNames, entryPoints), traceAPIDependency: (entryPointName: string, apiName: string): DependencyTree | null => traceAPIDependency(entryPointName, apiName, [...getUnreadyEntryPoints(), ...[...addedShells.values()].map(x => x.entryPoint)]), visualizeDependencyTree, performance: getPerformanceDebug(options, trace, memoizedArr) } if (typeof globalThis === 'undefined') { return } globalThis.repluggableAppDebug = { host, uniqueShellNames, extensionSlots, addedShells, readyAPIs, shellInstallers, utils, hmr: { hot } } }