diff --git a/src/repluggableAppDebug/debug.d.ts b/src/repluggableAppDebug/debug.d.ts index f288274..b6364cc 100644 --- a/src/repluggableAppDebug/debug.d.ts +++ b/src/repluggableAppDebug/debug.d.ts @@ -1,4 +1,4 @@ -import { EntryPoint, AppHost, AnySlotKey, LazyEntryPointFactory, PrivateShell } from '../API' +import { EntryPoint, AppHost, AnySlotKey, LazyEntryPointFactory, PrivateShell, SlotKey } from '../API' import { AnyExtensionSlot } from '../extensionSlot' import { Hot } from '../hot' @@ -23,6 +23,7 @@ export interface RepluggableAppDebugInfo { export interface RepluggableDebugUtils { apis(): APIDebugInfo[] unReadyEntryPoints(): EntryPoint[] + getRootUnreadyAPI: () => SlotKey[] whyEntryPointUnready(name: string): void findAPI(name: string): APIDebugInfo[] } diff --git a/src/repluggableAppDebug/repluggableAppDebug.ts b/src/repluggableAppDebug/repluggableAppDebug.ts index 435ee4d..5e5138b 100644 --- a/src/repluggableAppDebug/repluggableAppDebug.ts +++ b/src/repluggableAppDebug/repluggableAppDebug.ts @@ -37,6 +37,59 @@ function mapApiToEntryPoint(allPackages: EntryPoint[]) { return apiToEntryPoint } +/** + * a function that returns all the entry points in the system with their declared APIs and dependencies + */ +const getAllEntryPoints3 = () => { + return [ + ...window.repluggableAppDebug.utils.unReadyEntryPoints(), + ...[...window.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 = getAllEntryPoints3() + 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) { + // console.log('API not ready', currentAPI) + 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) + ) + // console.log('API declarer', declarer) + dependenciesOfUnreadyEntryPoint = declarer?.getDependencyAPIs?.() + } + } + return unReadyAPIsArray.reverse() + } +} + const getAPIOrEntryPointsDependencies = ( apisOrEntryPointsNames: string[], entryPoints: EntryPoint[] @@ -95,6 +148,7 @@ export function setupDebugInfo({ } }) }, + getRootUnreadyAPI: getRootUnreadyAPI(host), unReadyEntryPoints: (): EntryPoint[] => getUnreadyEntryPoints(), whyEntryPointUnready: (name: string) => { const unreadyEntryPoint = _.find( diff --git a/test/repluggableAppDebug.spec.ts b/test/repluggableAppDebug.spec.ts new file mode 100644 index 0000000..05f4960 --- /dev/null +++ b/test/repluggableAppDebug.spec.ts @@ -0,0 +1,118 @@ +import { createAppHost } from '../src' + +const unreadAPI = { name: 'unreadyAPI' } + +const getUnreadyAPIs = async () => { + await new Promise(resolve => setTimeout(resolve, 0)) + return window.repluggableAppDebug.utils.getRootUnreadyAPI() +} + +describe('RepluggableAppDebug', () => { + describe('getRootUnreadyAPI', () => { + it('should not report any issues in case there are no entry points', async () => { + createAppHost([]) + + const unreadyAPIs = await getUnreadyAPIs() + + expect(unreadyAPIs).toEqual([]) + }) + + it('should not report any issues in case all entry points are loaded', async () => { + createAppHost([ + { + name: 'entryPoint', + declareAPIs: () => [] + } + ]) + + const unreadyAPIs = await getUnreadyAPIs() + + expect(unreadyAPIs).toEqual([]) + }) + + it('should return an API if its not ready', async () => { + createAppHost([ + { + name: 'entryPoint', + getDependencyAPIs: () => [unreadAPI] + } + ]) + + const unreadyAPIs = await getUnreadyAPIs() + + expect(unreadyAPIs).toEqual([{ name: 'unreadyAPI' }]) + }) + + it('should return the root unready API when there are multiple entry points that depend on the same API', async () => { + createAppHost([ + { + name: 'entryPoint A', + getDependencyAPIs: () => [unreadAPI] + }, + { + name: 'entryPoint B', + getDependencyAPIs: () => [unreadAPI] + }, + { + name: 'entryPoint C', + getDependencyAPIs: () => [unreadAPI] + } + ]) + + const unreadyAPIs = await getUnreadyAPIs() + + expect(unreadyAPIs).toEqual([{ name: 'unreadyAPI' }]) + }) + + it('should return the root unready API when there is a graph of dependencies', async () => { + createAppHost([ + { + name: 'entryPoint A', + declareAPIs: () => [{ name: 'API A' }], + getDependencyAPIs: () => [{ name: 'API B' }] + }, + { + name: 'entryPoint B', + declareAPIs: () => [{ name: 'API B' }], + getDependencyAPIs: () => [{ name: 'API C' }] + }, + { + name: 'entryPoint C', + declareAPIs: () => [{ name: 'API C' }], + getDependencyAPIs: () => [unreadAPI] + } + ]) + + const unreadyAPIs = await getUnreadyAPIs() + + expect(unreadyAPIs).toEqual([{ name: 'unreadyAPI' }, { name: 'API C' }, { name: 'API B' }]) + }) + + it('should return the root unready API when there is a graph of dependencies (declation order is reversed)', async () => { + createAppHost( + [ + { + name: 'entryPoint A', + declareAPIs: () => [{ name: 'API A' }], + getDependencyAPIs: () => [{ name: 'API B' }] + }, + { + name: 'entryPoint B', + declareAPIs: () => [{ name: 'API B' }], + getDependencyAPIs: () => [{ name: 'API C' }] + }, + { + name: 'entryPoint C', + declareAPIs: () => [{ name: 'API C' }], + getDependencyAPIs: () => [unreadAPI] + } + ].reverse() + ) + + const unreadyAPIs = await getUnreadyAPIs() + // because we take the first unready entry point, and in this case its the root, + // we don't get the rest of the unready APIs + expect(unreadyAPIs).toEqual([{ name: 'unreadyAPI' }]) + }) + }) +})