Skip to content

Commit 5b3e45f

Browse files
committed
Address review comment
1 parent 4be2fd2 commit 5b3e45f

4 files changed

Lines changed: 109 additions & 113 deletions

File tree

packages/plugin/src/PluginUtils.tsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,108 @@ export function processLoadedModule(
385385
registerPlugin(pluginMap, name, moduleValue, version);
386386
}
387387
}
388+
389+
/**
390+
* Topologically sort plugins so that dependencies are loaded before the
391+
* plugins that depend on them. Plugins without dependencies or whose
392+
* dependencies are not in the manifest keep their original relative order
393+
* (stable sort). Throws if a dependency cycle is detected.
394+
*
395+
* @param plugins The plugin list from the manifest
396+
* @returns A new array with plugins sorted so dependencies come first
397+
*/
398+
export function sortPluginsByDependency<
399+
T extends Pick<PluginManifestPluginInfo, 'name' | 'package' | 'dependencies'>,
400+
>(plugins: readonly T[]): T[] {
401+
// Build a lookup from package name → plugin index
402+
const packageToIndex = new Map<string, number>();
403+
plugins.forEach((p, i) => {
404+
if (p.package != null) {
405+
packageToIndex.set(p.package, i);
406+
}
407+
});
408+
409+
// Build adjacency list: index → indices it depends on
410+
const depIndices = new Map<number, number[]>();
411+
plugins.forEach((p, i) => {
412+
if (p.dependencies != null && p.dependencies.length > 0) {
413+
const resolved: number[] = [];
414+
p.dependencies.forEach(dep => {
415+
const idx = packageToIndex.get(dep);
416+
if (idx != null) {
417+
resolved.push(idx);
418+
} else {
419+
log.warn(
420+
`Plugin '${p.name}' depends on '${dep}' which is not in the manifest`
421+
);
422+
}
423+
});
424+
if (resolved.length > 0) {
425+
depIndices.set(i, resolved);
426+
}
427+
}
428+
});
429+
430+
// If no plugin has in-manifest dependencies, return original order
431+
if (depIndices.size === 0) {
432+
return [...plugins];
433+
}
434+
435+
// Kahn's algorithm for topological sort (stable — preserves original order
436+
// among plugins at the same dependency depth)
437+
const inDegree = new Array<number>(plugins.length).fill(0);
438+
439+
// Reverse adjacency: who depends on me?
440+
const dependents = new Map<number, number[]>();
441+
depIndices.forEach((deps, idx) => {
442+
deps.forEach(dep => {
443+
if (!dependents.has(dep)) {
444+
dependents.set(dep, []);
445+
}
446+
const depList = dependents.get(dep);
447+
if (depList != null) {
448+
depList.push(idx);
449+
}
450+
inDegree[idx] += 1;
451+
});
452+
});
453+
454+
// Seed queue with all nodes that have no in-manifest dependencies,
455+
// in their original order
456+
const queue: number[] = [];
457+
for (let i = 0; i < plugins.length; i += 1) {
458+
if (inDegree[i] === 0) {
459+
queue.push(i);
460+
}
461+
}
462+
463+
const sorted: T[] = [];
464+
while (queue.length > 0) {
465+
const idx = queue.shift();
466+
if (idx == null) {
467+
break;
468+
}
469+
sorted.push(plugins[idx]);
470+
const deps = dependents.get(idx);
471+
if (deps != null) {
472+
// Process dependents in original manifest order for stability
473+
deps.sort((a, b) => a - b);
474+
deps.forEach(depIdx => {
475+
inDegree[depIdx] -= 1;
476+
if (inDegree[depIdx] === 0) {
477+
queue.push(depIdx);
478+
}
479+
});
480+
}
481+
}
482+
483+
if (sorted.length !== plugins.length) {
484+
// Find the cycle participants for a useful error message
485+
const inCycle = plugins.filter((_, i) => inDegree[i] > 0).map(p => p.name);
486+
throw new Error(
487+
`Circular plugin dependency detected among: ${inCycle.join(', ')}`
488+
);
489+
}
490+
491+
return sorted;
492+
}

packages/plugin/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export * from './PluginsContext';
22
export * from './PluginTypes';
33
export * from './PluginUtils';
4-
export * from './sortPluginsByDependency';
54
export * from './TablePlugin';
65
export * from './useCustomThemes';
76
export * from './useDashboardPlugins';

packages/plugin/src/sortPluginsByDependency.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { sortPluginsByDependency } from './sortPluginsByDependency';
2-
import { type PluginManifestPluginInfo } from './PluginUtils';
1+
import {
2+
sortPluginsByDependency,
3+
type PluginManifestPluginInfo,
4+
} from './PluginUtils';
35

46
function makeManifestPlugin(
57
name: string,

packages/plugin/src/sortPluginsByDependency.ts

Lines changed: 0 additions & 110 deletions
This file was deleted.

0 commit comments

Comments
 (0)