Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/repluggable-core/src/repluggableAppDebug/debug.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export interface DependencyTree {
export interface RepluggableDebugUtils {
apis(): APIDebugInfo[]
unReadyEntryPoints(): EntryPoint[]
getRootUnreadyAPIs(): AnySlotKey[]
/**
* @deprecated Use `getRootUnreadyAPIs` instead
*/
getRootUnreadyAPI(): SlotKey<any>
whyEntryPointUnready(name: string): void
findAPI(name: string): APIDebugInfo[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,57 +36,6 @@ function mapApiToEntryPoint(allPackages: 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<any>)
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 }>
Expand Down Expand Up @@ -198,6 +147,17 @@ export function setupDebugInfo({
shellInstallers,
performance: { options, trace, memoizedArr }
}: SetupDebugInfoParams) {
const getRootUnreadyAPIs = (): AnySlotKey[] => {
const unreadyEntryPoints = getUnreadyEntryPoints()
const unreadyEntryPointsDeclares = new Set(unreadyEntryPoints.flatMap(ep => (ep.declareAPIs?.() || []).map(key => key.name)))

const rootUnreadyAPIs = unreadyEntryPoints
.flatMap(ep => ep.getDependencyAPIs?.() || [])
.filter(key => !readyAPIs.has(getOwnSlotKey(key)) && !unreadyEntryPointsDeclares.has(key.name))

return _.uniqBy(rootUnreadyAPIs, 'name')
}

const utils = {
apis: () => {
return Array.from(readyAPIs).map((apiKey: AnySlotKey) => {
Expand All @@ -207,7 +167,16 @@ export function setupDebugInfo({
}
})
},
getRootUnreadyAPI: getRootUnreadyAPI(host),
/**
* dependencies of unready entry points that are neither ready nor declared by another
* unready entry point - the APIs actually blocking the host from loading
* (missing entry point or a contribution that never completed)
*/
getRootUnreadyAPIs,
/**
* @deprecated Use `getRootUnreadyAPIs` instead
*/
getRootUnreadyAPI: () => getRootUnreadyAPIs()[0],
unReadyEntryPoints: (): EntryPoint[] => getUnreadyEntryPoints(),
whyEntryPointUnready: (name: string) => {
const unreadyEntryPoint = _.find(
Expand Down
36 changes: 29 additions & 7 deletions packages/repluggable-core/test/repluggableAppDebug.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const unreadAPI = { name: 'unreadyAPI' }

const getUnreadyAPIs = async () => {
await new Promise(resolve => setTimeout(resolve, 0))
return globalThis.repluggableAppDebug.utils.getRootUnreadyAPI()
return globalThis.repluggableAppDebug.utils.getRootUnreadyAPIs()
}

describe('RepluggableAppDebug', () => {
Expand All @@ -14,7 +14,7 @@ describe('RepluggableAppDebug', () => {

const unreadyAPIs = await getUnreadyAPIs()

expect(unreadyAPIs).toBeUndefined()
expect(unreadyAPIs).toEqual([])
})

it('should not report any issues in case all entry points are loaded', async () => {
Expand All @@ -27,7 +27,7 @@ describe('RepluggableAppDebug', () => {

const unreadyAPIs = await getUnreadyAPIs()

expect(unreadyAPIs).toBeUndefined()
expect(unreadyAPIs).toEqual([])
})

it('should return an API if its not ready', async () => {
Expand All @@ -40,7 +40,7 @@ describe('RepluggableAppDebug', () => {

const unreadyAPIs = await getUnreadyAPIs()

expect(unreadyAPIs).toEqual({ name: 'unreadyAPI' })
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 () => {
Expand All @@ -61,7 +61,7 @@ describe('RepluggableAppDebug', () => {

const unreadyAPIs = await getUnreadyAPIs()

expect(unreadyAPIs).toEqual({ name: 'unreadyAPI' })
expect(unreadyAPIs).toEqual([{ name: 'unreadyAPI' }])
})

it('should return the root unready API when there is a graph of dependencies', async () => {
Expand All @@ -85,7 +85,29 @@ describe('RepluggableAppDebug', () => {

const unreadyAPIs = await getUnreadyAPIs()

expect(unreadyAPIs).toEqual({ name: 'unreadyAPI' })
expect(unreadyAPIs).toEqual([{ name: 'unreadyAPI' }])
})

it('should return all root unready APIs, excluding transitively blocked ones', async () => {
createAppHost([
{
name: 'entryPoint A',
getDependencyAPIs: () => [{ name: 'API B' }]
},
{
name: 'entryPoint B',
declareAPIs: () => [{ name: 'API B' }],
getDependencyAPIs: () => [{ name: 'missing B' }]
},
{
name: 'entryPoint C',
getDependencyAPIs: () => [{ name: 'missing C' }]
}
])

const unreadyAPIs = await getUnreadyAPIs()

expect(unreadyAPIs).toEqual([{ name: 'missing B' }, { name: 'missing C' }])
})

it('should return all traversal paths from an entry point to a transitive API', () => {
Expand Down Expand Up @@ -242,7 +264,7 @@ describe('RepluggableAppDebug', () => {
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' })
expect(unreadyAPIs).toEqual([{ name: 'unreadyAPI' }])
})
})
})
42 changes: 42 additions & 0 deletions packages/repluggable-core/test/testKit.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,5 +149,47 @@ describe('App Host TestKit', () => {
jest.runAllTimers()
await expect(hostPromise).rejects.toThrow(new RegExp(MockPublicAPI.name))
})

it('should report all root unready APIs of independent unready entry points', async () => {
const hostPromise = createAppHostAndWaitForLoading(
[
{
name: 'entryPoint A',
declareAPIs: () => [{ name: 'API A' }],
getDependencyAPIs: () => [{ name: 'missing A' }]
},
{
name: 'entryPoint B',
declareAPIs: () => [{ name: 'API B' }],
getDependencyAPIs: () => [{ name: 'missing B' }]
}
],
[]
)
jest.runAllTimers()
const error: Error = await hostPromise.then(
() => {
throw new Error('expected hostPromise to reject')
},
e => e
)

expect(error.message).toContain('"missing A"')
expect(error.message).toContain('"missing B"')
})

it('should respect a custom loading timeout', async () => {
let settled = false
const hostPromise = createAppHostAndWaitForLoading([dependsOnMockPackageEntryPoint], [], 10000)
hostPromise.catch(() => (settled = true))

jest.advanceTimersByTime(9999)
await Promise.resolve()
await Promise.resolve()
expect(settled).toBe(false)

jest.advanceTimersByTime(1)
await expect(hostPromise).rejects.toThrow('timed out after 10000ms')
})
})
})
16 changes: 11 additions & 5 deletions packages/repluggable-core/testKit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ export function createAppHostWithPacts(packages: EntryPointOrPackage[], pacts: P
})
}

export async function createAppHostAndWaitForLoading(packages: EntryPointOrPackage[], pacts: PactAPIBase[]): Promise<AppHost> {
export async function createAppHostAndWaitForLoading(
packages: EntryPointOrPackage[],
pacts: PactAPIBase[],
timeout: number = 3000
): Promise<AppHost> {
const appHost = createAppHostWithPacts(packages, pacts)
const declaredAPIs = _(packages)
.flatten()
Expand All @@ -92,17 +96,19 @@ export async function createAppHostAndWaitForLoading(packages: EntryPointOrPacka
setTimeout(() => {
const readyAPIs = Array.from(globalThis.repluggableAppDebug.readyAPIs)
const unreadyAPIs = declaredAPIs.filter(api => !readyAPIs.some(readyAPI => readyAPI.name === api.name))
const rootUnreadyAPI = globalThis.repluggableAppDebug.utils.getRootUnreadyAPI()
const rootUnreadyAPIs = globalThis.repluggableAppDebug.utils.getRootUnreadyAPIs()

reject(
new Error(
`createAppHostAndWaitForLoading - waiting for loading timed out.
there's a high chance this missing API is the main reason for it: ${JSON.stringify(rootUnreadyAPI)}
`createAppHostAndWaitForLoading - waiting for loading timed out after ${timeout}ms.
these root unready APIs are most likely the reason - unready entry points require them but nothing declares them (missing entry point or pact?): ${JSON.stringify(
rootUnreadyAPIs
)}

in addition here's the full list of declared APIs that have not been contributed: ${JSON.stringify(unreadyAPIs)}`
)
)
}, 3000)
}, timeout)
})

const loadingPromise = new Promise<void>(async resolve => {
Expand Down
Loading