Skip to content
Merged
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
11 changes: 11 additions & 0 deletions packages/repluggable-core/src/repluggableAppDebug/debug.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,23 @@ export interface RepluggableAppDebugInfo {
hmr: RepluggableHMR
}

export interface DependencyTree {
entryPoint: string
deps: { api: string; subtree: DependencyTree | null }[]
}

export interface RepluggableDebugUtils {
apis(): APIDebugInfo[]
unReadyEntryPoints(): EntryPoint[]
getRootUnreadyAPI(): SlotKey<any>
whyEntryPointUnready(name: string): void
findAPI(name: string): APIDebugInfo[]
getAPIOrEntryPointsDependencies(
apisOrEntryPointsNames: string[],
entryPoints?: EntryPoint[]
): { entryPoints: EntryPoint[]; apis: AnySlotKey[] }
traceAPIDependency(entryPointName: string, apiName: string): DependencyTree | null
Comment on lines +32 to +36

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These public signatures are inconsistent with the rest of the API and also with themselves.

Did you expose (getAPIOrEntryPointsDependencies) in public signature on purpose (+ isn't it weird that someone would need to provide an entrypoints array for the debug tool, or is there a use-case you know of)?

Why do you need entryPointName instead of deriving it by searching the declarer of the API?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just added types. The implementation itself is already there, and it accepts a list of entryPoints. Not sure if there are real use cases for that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EntryPointName is the starting point.
The goal is to visualize subtree of dependencies starting from this entry point to the specific API.

Use case: we have one entry point that we don't want to be dependant on the DocumentServicesAPI, because this entry point should be executed as early as possible (to show welcome screen). So adding documentServices even transitively will make it appear much later.

So this utility should make it easy to understand if provided entry point is dependant on the provided api, and if yes - visualise why exactly

visualizeDependencyTree(tree: DependencyTree | null): string
}

export interface RepluggableHMR {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,69 @@ const getRootUnreadyAPI = (host: AppHost) => {
}
}

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<string>()

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[]
Expand Down Expand Up @@ -167,6 +230,9 @@ export function setupDebugInfo({
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)]),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not reuse getAllEntryPoints?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It throws "not implemented" error in runtime

visualizeDependencyTree,
performance: getPerformanceDebug(options, trace, memoizedArr)
}

Expand Down
130 changes: 130 additions & 0 deletions packages/repluggable-core/test/repluggableAppDebug.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,136 @@ describe('RepluggableAppDebug', () => {
expect(unreadyAPIs).toEqual({ name: 'unreadyAPI' })
})

it('should return all traversal paths from an entry point to a transitive API', () => {
createAppHost([
{
name: 'entryPoint A',
getDependencyAPIs: () => [{ name: 'API B' }]
},
{
name: 'entryPoint B',
declareAPIs: () => [{ name: 'API B' }],
getDependencyAPIs: () => [{ name: 'API C' }]
},
{
name: 'entryPoint C',
declareAPIs: () => [{ name: 'API C' }]
}
])

const { traceAPIDependency } = globalThis.repluggableAppDebug.utils

expect(traceAPIDependency('entryPoint A', 'API C')).toEqual({
entryPoint: 'entryPoint A',
deps: [
{
api: 'API B',
subtree: {
entryPoint: 'entryPoint B',
deps: [{ api: 'API C', subtree: null }]
}
}
]
})

expect(traceAPIDependency('entryPoint A', 'API B')).toEqual({
entryPoint: 'entryPoint A',
deps: [{ api: 'API B', subtree: null }]
})

expect(traceAPIDependency('entryPoint C', 'API B')).toBeNull()
expect(traceAPIDependency('nonexistent', 'API B')).toBeNull()
})

it('should branch the dependency tree when multiple routes reach the target API', () => {
createAppHost([
{
name: 'entryPoint A',
getDependencyAPIs: () => [{ name: 'API X' }, { name: 'API Y' }]
},
{
name: 'entryPoint B',
declareAPIs: () => [{ name: 'API X' }],
getDependencyAPIs: () => [{ name: 'API Z' }]
},
{
name: 'entryPoint C',
declareAPIs: () => [{ name: 'API Y' }],
getDependencyAPIs: () => [{ name: 'API Z' }]
},
{
name: 'entryPoint D',
declareAPIs: () => [{ name: 'API Z' }]
}
])

const { traceAPIDependency } = globalThis.repluggableAppDebug.utils

expect(traceAPIDependency('entryPoint A', 'API Z')).toEqual({
entryPoint: 'entryPoint A',
deps: [
{
api: 'API X',
subtree: {
entryPoint: 'entryPoint B',
deps: [{ api: 'API Z', subtree: null }]
}
},
{
api: 'API Y',
subtree: {
entryPoint: 'entryPoint C',
deps: [{ api: 'API Z', subtree: null }]
}
}
]
})
})

it('should visualize the dependency tree, collapsing shared prefixes', () => {
createAppHost([
{
name: 'entryPoint A',
getDependencyAPIs: () => [{ name: 'API B' }]
},
{
name: 'entryPoint B',
declareAPIs: () => [{ name: 'API B' }],
getDependencyAPIs: () => [{ name: 'API X' }, { name: 'API Y' }]
},
{
name: 'entryPoint X',
declareAPIs: () => [{ name: 'API X' }],
getDependencyAPIs: () => [{ name: 'API Z' }]
},
{
name: 'entryPoint Y',
declareAPIs: () => [{ name: 'API Y' }],
getDependencyAPIs: () => [{ name: 'API Z' }]
},
{
name: 'entryPoint Z',
declareAPIs: () => [{ name: 'API Z' }]
}
])

const { traceAPIDependency, visualizeDependencyTree } = globalThis.repluggableAppDebug.utils
const tree = traceAPIDependency('entryPoint A', 'API Z')

expect(visualizeDependencyTree(tree)).toBe(
[
'entryPoint A',
'└─ API B (declared by entryPoint B)',
' ├─ API X (declared by entryPoint X)',
' │ └─ API Z',
' └─ API Y (declared by entryPoint Y)',
' └─ API Z'
].join('\n')
)

expect(visualizeDependencyTree(null)).toBe('(no dependency paths)')
})

it('should return the root unready API when there is a graph of dependencies (declation order is reversed)', async () => {
createAppHost(
[
Expand Down
Loading