11/**
22 * Dependency graph data structure and algorithms for multi-repo publishing.
33 *
4- * Provides `DependencyGraph` class with topological sort and cycle detection.
4+ * Provides `DependencyGraph` class with topological sort (via `@fuzdev/fuz_util/sort.js`)
5+ * and cycle detection by dependency type.
56 * For validation workflow and publishing order computation, see `graph_validation.ts`.
67 *
78 * @module
89 */
910
10- import type { LocalRepo } from './local_repo.js' ;
1111import { EMPTY_OBJECT } from '@fuzdev/fuz_util/object.js' ;
12+ import { topological_sort as topological_sort_generic } from '@fuzdev/fuz_util/sort.js' ;
13+
14+ import type { LocalRepo } from './local_repo.js' ;
1215
1316export const DEPENDENCY_TYPE = {
1417 PROD : 'prod' ,
@@ -121,120 +124,29 @@ export class DependencyGraph {
121124 /**
122125 * Computes topological sort order for dependency graph.
123126 *
124- * Uses Kahn's algorithm with alphabetical ordering within tiers for
125- * deterministic results. Throws if cycles detected.
127+ * Delegates to `@fuzdev/fuz_util/sort.js` for the sorting algorithm.
128+ * Throws if cycles detected.
126129 *
127130 * @param exclude_dev if true, excludes dev dependencies to break cycles.
128131 * Publishing uses exclude_dev=true to handle circular dev deps.
129132 * @returns array of package names in dependency order (dependencies before dependents)
130133 * @throws {Error } if circular dependencies detected in included dependency types
131134 */
132135 topological_sort ( exclude_dev = false ) : Array < string > {
133- const visited : Set < string > = new Set ( ) ;
134- const result : Array < string > = [ ] ;
135-
136- // Count incoming edges for each node
137- const in_degree : Map < string , number > = new Map ( ) ;
138- for ( const name of this . nodes . keys ( ) ) {
139- in_degree . set ( name , 0 ) ;
140- }
141- for ( const node of this . nodes . values ( ) ) {
142- for ( const [ dep_name , spec ] of node . dependencies ) {
143- // Skip dev dependencies if requested
144- if ( exclude_dev && spec . type === DEPENDENCY_TYPE . DEV ) continue ;
145-
146- if ( this . nodes . has ( dep_name ) ) {
147- in_degree . set ( node . name , in_degree . get ( node . name ) ! + 1 ) ;
148- }
149- }
150- }
151-
152- // Start with nodes that have no dependencies
153- const queue : Array < string > = [ ] ;
154- for ( const [ name , degree ] of in_degree ) {
155- if ( degree === 0 ) {
156- queue . push ( name ) ;
157- }
158- }
159-
160- // Sort initial queue alphabetically for deterministic ordering within tier
161- queue . sort ( ) ;
162-
163- // Process nodes
164- while ( queue . length > 0 ) {
165- const name = queue . shift ( ) ! ;
166- result . push ( name ) ;
167- visited . add ( name ) ;
168-
169- // Reduce in-degree for dependents
170- const node = this . nodes . get ( name ) ;
171- if ( node ) {
172- // Find packages that depend on this one
173- // Sort nodes to ensure deterministic iteration order
174- const sorted_nodes = Array . from ( this . nodes . values ( ) ) . sort ( ( a , b ) =>
175- a . name . localeCompare ( b . name ) ,
176- ) ;
177- for ( const other_node of sorted_nodes ) {
178- for ( const [ dep_name , spec ] of other_node . dependencies ) {
179- // Skip dev dependencies if requested
180- if ( exclude_dev && spec . type === DEPENDENCY_TYPE . DEV ) continue ;
181-
182- if ( dep_name === name ) {
183- const new_degree = in_degree . get ( other_node . name ) ! - 1 ;
184- in_degree . set ( other_node . name , new_degree ) ;
185- if ( new_degree === 0 ) {
186- queue . push ( other_node . name ) ;
187- }
188- }
189- }
190- }
191- }
192- }
193-
194- // Check for cycles
195- if ( result . length !== this . nodes . size ) {
196- const unvisited = Array . from ( this . nodes . keys ( ) ) . filter ( ( n ) => ! visited . has ( n ) ) ;
197- throw new Error ( `Circular dependency detected involving: ${ unvisited . join ( ', ' ) } ` ) ;
198- }
199-
200- return result ;
201- }
202-
203- detect_cycles ( ) : Array < Array < string > > {
204- const cycles : Array < Array < string > > = [ ] ;
205- const visited : Set < string > = new Set ( ) ;
206- const rec_stack : Set < string > = new Set ( ) ;
207-
208- const dfs = ( name : string , path : Array < string > ) : void => {
209- visited . add ( name ) ;
210- rec_stack . add ( name ) ;
211- path . push ( name ) ;
212-
213- const node = this . nodes . get ( name ) ;
214- if ( node ) {
215- for ( const [ dep_name ] of node . dependencies ) {
216- if ( this . nodes . has ( dep_name ) ) {
217- if ( ! visited . has ( dep_name ) ) {
218- dfs ( dep_name , [ ...path ] ) ;
219- } else if ( rec_stack . has ( dep_name ) ) {
220- // Found a cycle
221- const cycle_start = path . indexOf ( dep_name ) ;
222- cycles . push ( path . slice ( cycle_start ) . concat ( dep_name ) ) ;
223- }
224- }
225- }
226- }
227-
228- rec_stack . delete ( name ) ;
229- } ;
230-
231- for ( const name of this . nodes . keys ( ) ) {
232- if ( ! visited . has ( name ) ) {
233- dfs ( name , [ ] ) ;
234- }
136+ const items = Array . from ( this . nodes . values ( ) ) . map ( ( node ) => ( {
137+ id : node . name ,
138+ depends_on : Array . from ( node . dependencies . entries ( ) )
139+ . filter ( ( [ dep_name , spec ] ) => {
140+ if ( exclude_dev && spec . type === DEPENDENCY_TYPE . DEV ) return false ;
141+ return this . nodes . has ( dep_name ) ;
142+ } )
143+ . map ( ( [ dep_name ] ) => dep_name ) ,
144+ } ) ) ;
145+ const result = topological_sort_generic ( items , 'package' ) ;
146+ if ( ! result . ok ) {
147+ throw new Error ( result . error ) ;
235148 }
236-
237- return cycles ;
149+ return result . sorted . map ( ( item ) => item . id ) ;
238150 }
239151
240152 /**
@@ -252,94 +164,53 @@ export class DependencyGraph {
252164 production_cycles : Array < Array < string > > ;
253165 dev_cycles : Array < Array < string > > ;
254166 } {
255- const production_cycles : Array < Array < string > > = [ ] ;
256- const dev_cycles : Array < Array < string > > = [ ] ;
257- const visited_prod : Set < string > = new Set ( ) ;
258- const visited_dev : Set < string > = new Set ( ) ;
259- const rec_stack_prod : Set < string > = new Set ( ) ;
260- const rec_stack_dev : Set < string > = new Set ( ) ;
261-
262- // DFS for production/peer dependencies only
263- const dfs_prod = ( name : string , path : Array < string > ) : void => {
264- visited_prod . add ( name ) ;
265- rec_stack_prod . add ( name ) ;
266- path . push ( name ) ;
267-
268- const node = this . nodes . get ( name ) ;
269- if ( node ) {
270- for ( const [ dep_name , spec ] of node . dependencies ) {
271- // Skip dev dependencies
272- if ( spec . type === DEPENDENCY_TYPE . DEV ) continue ;
273-
274- if ( this . nodes . has ( dep_name ) ) {
275- if ( ! visited_prod . has ( dep_name ) ) {
276- dfs_prod ( dep_name , [ ...path ] ) ;
277- } else if ( rec_stack_prod . has ( dep_name ) ) {
278- // Found a production cycle
279- const cycle_start = path . indexOf ( dep_name ) ;
280- const cycle = path . slice ( cycle_start ) . concat ( dep_name ) ;
281- // Check if this cycle is unique
282- const cycle_key = [ ...cycle ] . sort ( ) . join ( ',' ) ;
283- const exists = production_cycles . some ( ( c ) => [ ...c ] . sort ( ) . join ( ',' ) === cycle_key ) ;
284- if ( ! exists ) {
285- production_cycles . push ( cycle ) ;
286- }
287- }
288- }
289- }
290- }
167+ const production_cycles = this . #find_cycles( ( spec ) => spec . type !== DEPENDENCY_TYPE . DEV ) ;
168+ const dev_cycles = this . #find_cycles( ( spec ) => spec . type === DEPENDENCY_TYPE . DEV ) ;
169+ return { production_cycles, dev_cycles} ;
170+ }
291171
292- rec_stack_prod . delete ( name ) ;
293- } ;
172+ /** DFS cycle detection following only edges that match the filter. */
173+ #find_cycles( include : ( spec : DependencySpec ) => boolean ) : Array < Array < string > > {
174+ const cycles : Array < Array < string > > = [ ] ;
175+ const visited : Set < string > = new Set ( ) ;
176+ const rec_stack : Set < string > = new Set ( ) ;
294177
295- // DFS for dev dependencies only
296- const dfs_dev = ( name : string , path : Array < string > ) : void => {
297- visited_dev . add ( name ) ;
298- rec_stack_dev . add ( name ) ;
178+ const dfs = ( name : string , path : Array < string > ) : void => {
179+ visited . add ( name ) ;
180+ rec_stack . add ( name ) ;
299181 path . push ( name ) ;
300182
301183 const node = this . nodes . get ( name ) ;
302184 if ( node ) {
303185 for ( const [ dep_name , spec ] of node . dependencies ) {
304- // Only check dev dependencies
305- if ( spec . type !== DEPENDENCY_TYPE . DEV ) continue ;
186+ if ( ! include ( spec ) ) continue ;
306187
307188 if ( this . nodes . has ( dep_name ) ) {
308- if ( ! visited_dev . has ( dep_name ) ) {
309- dfs_dev ( dep_name , [ ...path ] ) ;
310- } else if ( rec_stack_dev . has ( dep_name ) ) {
311- // Found a dev cycle
189+ if ( ! visited . has ( dep_name ) ) {
190+ dfs ( dep_name , [ ...path ] ) ;
191+ } else if ( rec_stack . has ( dep_name ) ) {
312192 const cycle_start = path . indexOf ( dep_name ) ;
313193 const cycle = path . slice ( cycle_start ) . concat ( dep_name ) ;
314- // Check if this cycle is unique
315194 const cycle_key = [ ...cycle ] . sort ( ) . join ( ',' ) ;
316- const exists = dev_cycles . some ( ( c ) => [ ...c ] . sort ( ) . join ( ',' ) === cycle_key ) ;
195+ const exists = cycles . some ( ( c ) => [ ...c ] . sort ( ) . join ( ',' ) === cycle_key ) ;
317196 if ( ! exists ) {
318- dev_cycles . push ( cycle ) ;
197+ cycles . push ( cycle ) ;
319198 }
320199 }
321200 }
322201 }
323202 }
324203
325- rec_stack_dev . delete ( name ) ;
204+ rec_stack . delete ( name ) ;
326205 } ;
327206
328- // Check for production/peer cycles
329- for ( const name of this . nodes . keys ( ) ) {
330- if ( ! visited_prod . has ( name ) ) {
331- dfs_prod ( name , [ ] ) ;
332- }
333- }
334-
335- // Check for dev cycles
336207 for ( const name of this . nodes . keys ( ) ) {
337- if ( ! visited_dev . has ( name ) ) {
338- dfs_dev ( name , [ ] ) ;
208+ if ( ! visited . has ( name ) ) {
209+ dfs ( name , [ ] ) ;
339210 }
340211 }
341212
342- return { production_cycles , dev_cycles } ;
213+ return cycles ;
343214 }
344215
345216 toJSON ( ) : DependencyGraphJson {
0 commit comments