@@ -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+ }
0 commit comments