@eldrforge/tree-core 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +1112 -46
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,25 +1,24 @@
1
1
  # @eldrforge/tree-core
2
2
 
3
- Dependency graph algorithms for monorepo package analysis.
3
+ A powerful TypeScript library for analyzing and managing dependencies in monorepo workspaces. Build dependency graphs, perform topological sorting, detect circular dependencies, and determine optimal build orders for complex package ecosystems.
4
4
 
5
- ## Test Coverage
6
-
7
- **94.11% coverage** āœ… (exceeds 80% target)
8
-
9
- ```
10
- File | % Stmts | % Branch | % Funcs | % Lines
11
- -------------------|---------|----------|---------|--------
12
- dependencyGraph.ts | 94.11 | 90.76 | 80 | 94.11
13
- ```
5
+ [![Test Coverage](https://img.shields.io/badge/coverage-94.11%25-brightgreen.svg)](./coverage)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](https://www.typescriptlang.org/)
7
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
8
+ [![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org)
14
9
 
15
10
  ## Features
16
11
 
17
- - šŸ“¦ **Package Discovery** - Scan workspace for package.json files
18
- - šŸ” **Dependency Analysis** - Build dependency graphs
19
- - šŸ“Š **Topological Sort** - Determine build order
20
- - āš ļø **Circular Detection** - Identify circular dependencies
21
- - šŸŽÆ **Pattern Filtering** - Exclude packages by pattern
22
- - šŸ’¾ **Serialization** - Save/load graphs for checkpointing
12
+ - šŸ“¦ **Package Discovery** - Recursively scan directories for package.json files
13
+ - šŸ” **Dependency Analysis** - Build comprehensive dependency graphs with local and external dependencies
14
+ - šŸ“Š **Topological Sort** - Determine optimal build/execution order respecting dependencies
15
+ - āš ļø **Circular Detection** - Identify and report circular dependencies with clear error messages
16
+ - šŸŽÆ **Pattern Filtering** - Exclude packages using glob patterns (e.g., node_modules, test fixtures)
17
+ - šŸ”„ **Reverse Dependencies** - Find all packages that depend on a given package
18
+ - šŸ’¾ **Serialization** - Save and restore graphs for checkpointing and caching
19
+ - āœ… **Graph Validation** - Comprehensive integrity checks for dependency graphs
20
+ - 🪵 **Custom Logging** - Configurable logger interface for integration with any logging system
21
+ - šŸ“ **TypeScript First** - Full type safety with comprehensive TypeScript definitions
23
22
 
24
23
  ## Installation
25
24
 
@@ -27,76 +26,1143 @@ dependencyGraph.ts | 94.11 | 90.76 | 80 | 94.11
27
26
  npm install @eldrforge/tree-core
28
27
  ```
29
28
 
30
- ## Usage
29
+ **Requirements:**
30
+ - Node.js >= 18.0.0
31
+ - TypeScript >= 5.0 (if using TypeScript)
32
+
33
+ ## Quick Start
31
34
 
32
35
  ```typescript
33
36
  import {
34
37
  scanForPackageJsonFiles,
35
38
  buildDependencyGraph,
36
- topologicalSort
39
+ topologicalSort,
40
+ validateGraph
37
41
  } from '@eldrforge/tree-core';
38
42
 
39
- // Scan workspace for packages
40
- const packagePaths = await scanForPackageJsonFiles('/path/to/workspace');
43
+ // 1. Scan your monorepo for packages
44
+ const packagePaths = await scanForPackageJsonFiles('/path/to/monorepo', [
45
+ '**/node_modules/**',
46
+ '**/dist/**'
47
+ ]);
41
48
 
42
- // Build dependency graph
49
+ // 2. Build the dependency graph
43
50
  const graph = await buildDependencyGraph(packagePaths);
44
51
 
45
- // Get build order
52
+ // 3. Validate the graph (optional but recommended)
53
+ const validation = validateGraph(graph);
54
+ if (!validation.valid) {
55
+ console.error('Graph validation errors:', validation.errors);
56
+ process.exit(1);
57
+ }
58
+
59
+ // 4. Get the correct build order
46
60
  const buildOrder = topologicalSort(graph);
47
61
 
48
- console.log('Build order:', buildOrder);
62
+ console.log('Build packages in this order:', buildOrder);
63
+
64
+ // 5. Execute builds in order
65
+ for (const packageName of buildOrder) {
66
+ const pkg = graph.packages.get(packageName);
67
+ console.log(`Building ${packageName} at ${pkg.path}`);
68
+ // Run your build command here
69
+ }
70
+ ```
71
+
72
+ ## Core Concepts
73
+
74
+ ### PackageInfo
75
+
76
+ Represents information about a single package in your workspace:
77
+
78
+ ```typescript
79
+ interface PackageInfo {
80
+ name: string; // Package name from package.json
81
+ version: string; // Package version
82
+ path: string; // Absolute path to package directory
83
+ dependencies: Set<string>; // All dependencies (including dev, peer, optional)
84
+ devDependencies: Set<string>; // Development dependencies only
85
+ localDependencies: Set<string>; // Dependencies on other workspace packages
86
+ }
87
+ ```
88
+
89
+ ### DependencyGraph
90
+
91
+ The core data structure representing package relationships:
92
+
93
+ ```typescript
94
+ interface DependencyGraph {
95
+ packages: Map<string, PackageInfo>; // All packages by name
96
+ edges: Map<string, Set<string>>; // Package -> Dependencies
97
+ reverseEdges: Map<string, Set<string>>; // Package -> Dependents
98
+ }
99
+ ```
100
+
101
+ ## Detailed Usage Examples
102
+
103
+ ### Example 1: Basic Monorepo Analysis
104
+
105
+ ```typescript
106
+ import {
107
+ scanForPackageJsonFiles,
108
+ buildDependencyGraph,
109
+ topologicalSort
110
+ } from '@eldrforge/tree-core';
111
+
112
+ async function analyzeMonorepo(workspaceRoot: string) {
113
+ // Find all packages, excluding common directories
114
+ const packagePaths = await scanForPackageJsonFiles(workspaceRoot, [
115
+ '**/node_modules/**',
116
+ '**/dist/**',
117
+ '**/build/**',
118
+ '**/.git/**'
119
+ ]);
120
+
121
+ console.log(`Found ${packagePaths.length} packages`);
122
+
123
+ // Build dependency graph
124
+ const graph = await buildDependencyGraph(packagePaths);
125
+
126
+ // Get build order
127
+ const buildOrder = topologicalSort(graph);
128
+
129
+ // Display results
130
+ console.log('\nBuild Order:');
131
+ buildOrder.forEach((pkg, index) => {
132
+ const info = graph.packages.get(pkg);
133
+ const localDeps = Array.from(info.localDependencies);
134
+ console.log(`${index + 1}. ${pkg} (${localDeps.length} local deps)`);
135
+ if (localDeps.length > 0) {
136
+ console.log(` Depends on: ${localDeps.join(', ')}`);
137
+ }
138
+ });
139
+
140
+ return { graph, buildOrder };
141
+ }
142
+
143
+ // Usage
144
+ analyzeMonorepo('/path/to/my-monorepo');
145
+ ```
146
+
147
+ ### Example 2: Finding Affected Packages
148
+
149
+ When you change a package, find all packages that need to be rebuilt:
150
+
151
+ ```typescript
152
+ import {
153
+ buildDependencyGraph,
154
+ findAllDependents,
155
+ scanForPackageJsonFiles
156
+ } from '@eldrforge/tree-core';
157
+
158
+ async function findAffectedPackages(
159
+ workspaceRoot: string,
160
+ changedPackageName: string
161
+ ): Promise<string[]> {
162
+ const packagePaths = await scanForPackageJsonFiles(workspaceRoot);
163
+ const graph = await buildDependencyGraph(packagePaths);
164
+
165
+ // Find all packages that depend on the changed package
166
+ const dependents = findAllDependents(changedPackageName, graph);
167
+
168
+ // Include the changed package itself
169
+ const affected = [changedPackageName, ...Array.from(dependents)];
170
+
171
+ console.log(`Package ${changedPackageName} affects:`);
172
+ affected.forEach(pkg => {
173
+ const info = graph.packages.get(pkg);
174
+ console.log(` - ${pkg} (${info.path})`);
175
+ });
176
+
177
+ return affected;
178
+ }
179
+
180
+ // Usage
181
+ const affected = await findAffectedPackages(
182
+ '/path/to/monorepo',
183
+ '@myorg/core-utils'
184
+ );
185
+ ```
186
+
187
+ ### Example 3: Detecting Circular Dependencies
188
+
189
+ ```typescript
190
+ import {
191
+ buildDependencyGraph,
192
+ topologicalSort,
193
+ validateGraph,
194
+ scanForPackageJsonFiles
195
+ } from '@eldrforge/tree-core';
196
+
197
+ async function checkForCircularDependencies(workspaceRoot: string) {
198
+ const packagePaths = await scanForPackageJsonFiles(workspaceRoot);
199
+ const graph = await buildDependencyGraph(packagePaths);
200
+
201
+ // Method 1: Use validateGraph
202
+ const validation = validateGraph(graph);
203
+ if (!validation.valid) {
204
+ console.error('āŒ Graph validation failed:');
205
+ validation.errors.forEach(error => console.error(` - ${error}`));
206
+ return false;
207
+ }
208
+
209
+ // Method 2: Use topologicalSort (throws on circular deps)
210
+ try {
211
+ const buildOrder = topologicalSort(graph);
212
+ console.log('āœ… No circular dependencies detected');
213
+ console.log(` ${buildOrder.length} packages can be built in order`);
214
+ return true;
215
+ } catch (error) {
216
+ console.error('āŒ Circular dependency detected:', error.message);
217
+ return false;
218
+ }
219
+ }
220
+
221
+ // Usage
222
+ const isValid = await checkForCircularDependencies('/path/to/monorepo');
223
+ ```
224
+
225
+ ### Example 4: Parallel Build Planning
226
+
227
+ Determine which packages can be built in parallel:
228
+
229
+ ```typescript
230
+ import {
231
+ buildDependencyGraph,
232
+ topologicalSort,
233
+ scanForPackageJsonFiles,
234
+ type DependencyGraph
235
+ } from '@eldrforge/tree-core';
236
+
237
+ function planParallelBuilds(graph: DependencyGraph): string[][] {
238
+ const buildOrder = topologicalSort(graph);
239
+ const levels: string[][] = [];
240
+ const packageLevel = new Map<string, number>();
241
+
242
+ // Assign each package to a build level
243
+ for (const pkg of buildOrder) {
244
+ const deps = graph.edges.get(pkg) || new Set();
245
+ const maxDepLevel = Math.max(
246
+ -1,
247
+ ...Array.from(deps).map(dep => packageLevel.get(dep) ?? -1)
248
+ );
249
+ const level = maxDepLevel + 1;
250
+
251
+ packageLevel.set(pkg, level);
252
+
253
+ // Ensure level array exists
254
+ while (levels.length <= level) {
255
+ levels.push([]);
256
+ }
257
+
258
+ levels[level].push(pkg);
259
+ }
260
+
261
+ return levels;
262
+ }
263
+
264
+ async function parallelBuild(workspaceRoot: string) {
265
+ const packagePaths = await scanForPackageJsonFiles(workspaceRoot);
266
+ const graph = await buildDependencyGraph(packagePaths);
267
+ const buildLevels = planParallelBuilds(graph);
268
+
269
+ console.log(`Build plan with ${buildLevels.length} parallel stages:\n`);
270
+
271
+ for (let i = 0; i < buildLevels.length; i++) {
272
+ const level = buildLevels[i];
273
+ console.log(`Stage ${i + 1} (${level.length} packages in parallel):`);
274
+ level.forEach(pkg => console.log(` - ${pkg}`));
275
+ console.log();
276
+
277
+ // Build all packages in this level in parallel
278
+ await Promise.all(
279
+ level.map(async (pkg) => {
280
+ const info = graph.packages.get(pkg);
281
+ console.log(`Building ${pkg}...`);
282
+ // Execute build command here
283
+ })
284
+ );
285
+ }
286
+ }
287
+
288
+ // Usage
289
+ await parallelBuild('/path/to/monorepo');
49
290
  ```
50
291
 
51
- ## API
292
+ ### Example 5: Graph Serialization for Caching
293
+
294
+ Save and restore dependency graphs to avoid rescanning:
295
+
296
+ ```typescript
297
+ import {
298
+ buildDependencyGraph,
299
+ serializeGraph,
300
+ deserializeGraph,
301
+ scanForPackageJsonFiles,
302
+ type SerializedGraph
303
+ } from '@eldrforge/tree-core';
304
+ import fs from 'fs/promises';
305
+
306
+ async function getCachedGraph(
307
+ workspaceRoot: string,
308
+ cacheFile: string
309
+ ): Promise<DependencyGraph> {
310
+ try {
311
+ // Try to load from cache
312
+ const cached = await fs.readFile(cacheFile, 'utf-8');
313
+ const serialized: SerializedGraph = JSON.parse(cached);
314
+ console.log('āœ… Loaded graph from cache');
315
+ return deserializeGraph(serialized);
316
+ } catch {
317
+ // Cache miss - build and save
318
+ console.log('šŸ“Š Building dependency graph...');
319
+ const packagePaths = await scanForPackageJsonFiles(workspaceRoot);
320
+ const graph = await buildDependencyGraph(packagePaths);
321
+
322
+ // Save to cache
323
+ const serialized = serializeGraph(graph);
324
+ await fs.writeFile(cacheFile, JSON.stringify(serialized, null, 2));
325
+ console.log('šŸ’¾ Saved graph to cache');
326
+
327
+ return graph;
328
+ }
329
+ }
330
+
331
+ // Usage
332
+ const graph = await getCachedGraph(
333
+ '/path/to/monorepo',
334
+ '.cache/dependency-graph.json'
335
+ );
336
+ ```
337
+
338
+ ### Example 6: Custom Logging Integration
339
+
340
+ Integrate with your logging system:
341
+
342
+ ```typescript
343
+ import { setLogger, buildDependencyGraph } from '@eldrforge/tree-core';
344
+ import winston from 'winston'; // or any other logger
345
+
346
+ // Create your logger
347
+ const logger = winston.createLogger({
348
+ level: 'info',
349
+ format: winston.format.json(),
350
+ transports: [new winston.transports.Console()]
351
+ });
352
+
353
+ // Configure tree-core to use your logger
354
+ setLogger({
355
+ info: (msg, ...args) => logger.info(msg, ...args),
356
+ error: (msg, ...args) => logger.error(msg, ...args),
357
+ warn: (msg, ...args) => logger.warn(msg, ...args),
358
+ verbose: (msg, ...args) => logger.verbose(msg, ...args),
359
+ debug: (msg, ...args) => logger.debug(msg, ...args)
360
+ });
361
+
362
+ // Now all tree-core operations will log through your logger
363
+ const graph = await buildDependencyGraph(packagePaths);
364
+ ```
365
+
366
+ ### Example 7: Selective Package Analysis
367
+
368
+ Analyze only specific packages or patterns:
369
+
370
+ ```typescript
371
+ import {
372
+ scanForPackageJsonFiles,
373
+ buildDependencyGraph,
374
+ type DependencyGraph
375
+ } from '@eldrforge/tree-core';
376
+
377
+ async function analyzePackageSubset(
378
+ workspaceRoot: string,
379
+ includePatterns: string[]
380
+ ): Promise<DependencyGraph> {
381
+ // Scan for all packages
382
+ const allPackages = await scanForPackageJsonFiles(workspaceRoot);
383
+
384
+ // Filter to matching packages
385
+ const selectedPackages = allPackages.filter(pkgPath =>
386
+ includePatterns.some(pattern => pkgPath.includes(pattern))
387
+ );
388
+
389
+ console.log(`Analyzing ${selectedPackages.length} packages`);
390
+
391
+ // Build graph for selected packages only
392
+ const graph = await buildDependencyGraph(selectedPackages);
393
+
394
+ return graph;
395
+ }
396
+
397
+ // Usage: Analyze only packages in the 'services' directory
398
+ const graph = await analyzePackageSubset(
399
+ '/path/to/monorepo',
400
+ ['packages/services/']
401
+ );
402
+ ```
403
+
404
+ ### Example 8: Dependency Report Generation
405
+
406
+ Generate a comprehensive dependency report:
407
+
408
+ ```typescript
409
+ import {
410
+ scanForPackageJsonFiles,
411
+ buildDependencyGraph,
412
+ type DependencyGraph
413
+ } from '@eldrforge/tree-core';
414
+
415
+ interface DependencyReport {
416
+ totalPackages: number;
417
+ totalLocalDependencies: number;
418
+ packagesWithNoDependencies: string[];
419
+ packagesWithNoDependents: string[];
420
+ mostDependedOn: Array<{ name: string; dependents: number }>;
421
+ mostDependencies: Array<{ name: string; dependencies: number }>;
422
+ }
423
+
424
+ function generateReport(graph: DependencyGraph): DependencyReport {
425
+ const packages = Array.from(graph.packages.values());
426
+
427
+ // Find packages with no dependencies
428
+ const noDeps = packages
429
+ .filter(pkg => pkg.localDependencies.size === 0)
430
+ .map(pkg => pkg.name);
431
+
432
+ // Find packages with no dependents
433
+ const noDependents = packages
434
+ .filter(pkg => {
435
+ const dependents = graph.reverseEdges.get(pkg.name);
436
+ return !dependents || dependents.size === 0;
437
+ })
438
+ .map(pkg => pkg.name);
439
+
440
+ // Find most depended-on packages
441
+ const mostDependedOn = packages
442
+ .map(pkg => ({
443
+ name: pkg.name,
444
+ dependents: (graph.reverseEdges.get(pkg.name) || new Set()).size
445
+ }))
446
+ .sort((a, b) => b.dependents - a.dependents)
447
+ .slice(0, 5);
448
+
449
+ // Find packages with most dependencies
450
+ const mostDeps = packages
451
+ .map(pkg => ({
452
+ name: pkg.name,
453
+ dependencies: pkg.localDependencies.size
454
+ }))
455
+ .sort((a, b) => b.dependencies - a.dependencies)
456
+ .slice(0, 5);
457
+
458
+ // Calculate total local dependencies
459
+ const totalLocalDeps = packages.reduce(
460
+ (sum, pkg) => sum + pkg.localDependencies.size,
461
+ 0
462
+ );
463
+
464
+ return {
465
+ totalPackages: packages.length,
466
+ totalLocalDependencies: totalLocalDeps,
467
+ packagesWithNoDependencies: noDeps,
468
+ packagesWithNoDependents: noDependents,
469
+ mostDependedOn,
470
+ mostDependencies: mostDeps
471
+ };
472
+ }
473
+
474
+ async function printDependencyReport(workspaceRoot: string) {
475
+ const packagePaths = await scanForPackageJsonFiles(workspaceRoot);
476
+ const graph = await buildDependencyGraph(packagePaths);
477
+ const report = generateReport(graph);
478
+
479
+ console.log('šŸ“Š Dependency Report\n');
480
+ console.log(`Total Packages: ${report.totalPackages}`);
481
+ console.log(`Total Local Dependencies: ${report.totalLocalDependencies}`);
482
+ console.log(`Average Dependencies per Package: ${(
483
+ report.totalLocalDependencies / report.totalPackages
484
+ ).toFixed(2)}\n`);
485
+
486
+ console.log('šŸ“¦ Leaf Packages (no dependencies):');
487
+ report.packagesWithNoDependencies.forEach(pkg => console.log(` - ${pkg}`));
488
+
489
+ console.log('\n🌲 Root Packages (no dependents):');
490
+ report.packagesWithNoDependents.forEach(pkg => console.log(` - ${pkg}`));
491
+
492
+ console.log('\n⭐ Most Depended-On Packages:');
493
+ report.mostDependedOn.forEach(({ name, dependents }) => {
494
+ console.log(` - ${name} (${dependents} packages depend on it)`);
495
+ });
496
+
497
+ console.log('\nšŸ”— Packages with Most Dependencies:');
498
+ report.mostDependencies.forEach(({ name, dependencies }) => {
499
+ console.log(` - ${name} (depends on ${dependencies} packages)`);
500
+ });
501
+ }
502
+
503
+ // Usage
504
+ await printDependencyReport('/path/to/monorepo');
505
+ ```
506
+
507
+ ## API Reference
52
508
 
53
509
  ### Package Discovery
54
510
 
55
- - `scanForPackageJsonFiles(directory, excludedPatterns?)` - Find all package.json files
56
- - `parsePackageJson(packageJsonPath)` - Parse and validate a package.json
57
- - `shouldExclude(packagePath, excludedPatterns)` - Check if package should be excluded
511
+ #### `scanForPackageJsonFiles(directory, excludedPatterns?)`
512
+
513
+ Recursively scans a directory for package.json files.
514
+
515
+ ```typescript
516
+ async function scanForPackageJsonFiles(
517
+ directory: string,
518
+ excludedPatterns?: string[]
519
+ ): Promise<string[]>
520
+ ```
521
+
522
+ **Parameters:**
523
+ - `directory` - Root directory to scan
524
+ - `excludedPatterns` - Optional glob patterns to exclude (e.g., `**/node_modules/**`)
525
+
526
+ **Returns:** Array of absolute paths to package.json files
527
+
528
+ **Example:**
529
+ ```typescript
530
+ const packages = await scanForPackageJsonFiles('/workspace', [
531
+ '**/node_modules/**',
532
+ '**/dist/**',
533
+ '**/.git/**'
534
+ ]);
535
+ ```
536
+
537
+ #### `parsePackageJson(packageJsonPath)`
538
+
539
+ Parses and validates a single package.json file.
540
+
541
+ ```typescript
542
+ async function parsePackageJson(packageJsonPath: string): Promise<PackageInfo>
543
+ ```
544
+
545
+ **Parameters:**
546
+ - `packageJsonPath` - Absolute path to package.json file
547
+
548
+ **Returns:** `PackageInfo` object
549
+
550
+ **Throws:** Error if file doesn't exist, is invalid JSON, or missing required fields
551
+
552
+ **Example:**
553
+ ```typescript
554
+ const pkg = await parsePackageJson('/workspace/packages/core/package.json');
555
+ console.log(`Package: ${pkg.name}@${pkg.version}`);
556
+ console.log(`Dependencies: ${Array.from(pkg.dependencies).join(', ')}`);
557
+ ```
558
+
559
+ #### `shouldExclude(packagePath, excludedPatterns)`
560
+
561
+ Checks if a package path matches exclusion patterns.
562
+
563
+ ```typescript
564
+ function shouldExclude(
565
+ packageJsonPath: string,
566
+ excludedPatterns: string[]
567
+ ): boolean
568
+ ```
569
+
570
+ **Parameters:**
571
+ - `packageJsonPath` - Path to check
572
+ - `excludedPatterns` - Array of glob patterns
573
+
574
+ **Returns:** `true` if path should be excluded
575
+
576
+ **Example:**
577
+ ```typescript
578
+ const shouldSkip = shouldExclude(
579
+ '/workspace/node_modules/pkg/package.json',
580
+ ['**/node_modules/**']
581
+ ); // true
582
+ ```
58
583
 
59
584
  ### Graph Building
60
585
 
61
- - `buildDependencyGraph(packageJsonPaths)` - Build dependency graph from packages
62
- - `buildReverseGraph(edges)` - Create reverse dependency map
586
+ #### `buildDependencyGraph(packageJsonPaths)`
587
+
588
+ Builds a complete dependency graph from package.json files.
589
+
590
+ ```typescript
591
+ async function buildDependencyGraph(
592
+ packageJsonPaths: string[]
593
+ ): Promise<DependencyGraph>
594
+ ```
595
+
596
+ **Parameters:**
597
+ - `packageJsonPaths` - Array of paths to package.json files (from `scanForPackageJsonFiles`)
598
+
599
+ **Returns:** Complete `DependencyGraph` with packages, edges, and reverse edges
600
+
601
+ **Example:**
602
+ ```typescript
603
+ const paths = await scanForPackageJsonFiles('/workspace');
604
+ const graph = await buildDependencyGraph(paths);
605
+
606
+ // Access packages
607
+ for (const [name, info] of graph.packages) {
608
+ console.log(`${name}: ${info.localDependencies.size} local deps`);
609
+ }
610
+ ```
611
+
612
+ #### `buildReverseGraph(edges)`
613
+
614
+ Creates a reverse dependency map (package -> dependents).
615
+
616
+ ```typescript
617
+ function buildReverseGraph(
618
+ edges: Map<string, Set<string>>
619
+ ): Map<string, Set<string>>
620
+ ```
621
+
622
+ **Parameters:**
623
+ - `edges` - Forward dependency edges (package -> dependencies)
624
+
625
+ **Returns:** Reverse edges (package -> dependents)
626
+
627
+ **Example:**
628
+ ```typescript
629
+ const reverseEdges = buildReverseGraph(graph.edges);
630
+ const dependents = reverseEdges.get('@myorg/core') || new Set();
631
+ console.log(`Packages that depend on @myorg/core: ${Array.from(dependents)}`);
632
+ ```
63
633
 
64
634
  ### Graph Analysis
65
635
 
66
- - `topologicalSort(graph)` - Determine execution order
67
- - `findAllDependents(packageName, graph)` - Find all packages that depend on a package
68
- - `validateGraph(graph)` - Check graph integrity
636
+ #### `topologicalSort(graph)`
637
+
638
+ Performs topological sort to determine build order.
639
+
640
+ ```typescript
641
+ function topologicalSort(graph: DependencyGraph): string[]
642
+ ```
643
+
644
+ **Parameters:**
645
+ - `graph` - Dependency graph
646
+
647
+ **Returns:** Array of package names in dependency order (dependencies first)
648
+
649
+ **Throws:** Error if circular dependencies detected
650
+
651
+ **Example:**
652
+ ```typescript
653
+ try {
654
+ const buildOrder = topologicalSort(graph);
655
+ console.log('Build order:', buildOrder);
656
+ } catch (error) {
657
+ console.error('Circular dependency:', error.message);
658
+ }
659
+ ```
660
+
661
+ #### `findAllDependents(packageName, graph)`
662
+
663
+ Finds all packages that depend on a given package (transitively).
664
+
665
+ ```typescript
666
+ function findAllDependents(
667
+ packageName: string,
668
+ graph: DependencyGraph
669
+ ): Set<string>
670
+ ```
671
+
672
+ **Parameters:**
673
+ - `packageName` - Package to find dependents for
674
+ - `graph` - Dependency graph
675
+
676
+ **Returns:** Set of package names that depend on the given package
677
+
678
+ **Example:**
679
+ ```typescript
680
+ const dependents = findAllDependents('@myorg/utils', graph);
681
+ console.log(`${dependents.size} packages depend on @myorg/utils:`);
682
+ for (const dep of dependents) {
683
+ console.log(` - ${dep}`);
684
+ }
685
+ ```
686
+
687
+ #### `validateGraph(graph)`
688
+
689
+ Validates graph integrity and detects issues.
690
+
691
+ ```typescript
692
+ function validateGraph(graph: DependencyGraph): {
693
+ valid: boolean;
694
+ errors: string[];
695
+ }
696
+ ```
697
+
698
+ **Parameters:**
699
+ - `graph` - Dependency graph to validate
700
+
701
+ **Returns:** Object with `valid` boolean and array of error messages
702
+
703
+ **Example:**
704
+ ```typescript
705
+ const result = validateGraph(graph);
706
+ if (!result.valid) {
707
+ console.error('Graph validation failed:');
708
+ result.errors.forEach(err => console.error(` - ${err}`));
709
+ process.exit(1);
710
+ }
711
+ ```
69
712
 
70
713
  ### Serialization
71
714
 
72
- - `serializeGraph(graph)` - Convert graph to JSON-serializable format
73
- - `deserializeGraph(serialized)` - Restore graph from serialized format
715
+ #### `serializeGraph(graph)`
716
+
717
+ Converts graph to JSON-serializable format.
718
+
719
+ ```typescript
720
+ function serializeGraph(graph: DependencyGraph): SerializedGraph
721
+ ```
722
+
723
+ **Parameters:**
724
+ - `graph` - Dependency graph
725
+
726
+ **Returns:** Serialized graph object (can be JSON.stringify'd)
727
+
728
+ **Example:**
729
+ ```typescript
730
+ const serialized = serializeGraph(graph);
731
+ await fs.writeFile('graph.json', JSON.stringify(serialized, null, 2));
732
+ ```
733
+
734
+ #### `deserializeGraph(serialized)`
735
+
736
+ Restores graph from serialized format.
737
+
738
+ ```typescript
739
+ function deserializeGraph(serialized: SerializedGraph): DependencyGraph
740
+ ```
741
+
742
+ **Parameters:**
743
+ - `serialized` - Serialized graph object
744
+
745
+ **Returns:** Restored dependency graph
746
+
747
+ **Example:**
748
+ ```typescript
749
+ const data = await fs.readFile('graph.json', 'utf-8');
750
+ const serialized = JSON.parse(data);
751
+ const graph = deserializeGraph(serialized);
752
+ ```
753
+
754
+ ### Configuration
755
+
756
+ #### `setLogger(logger)`
757
+
758
+ Configures custom logger for all tree-core operations.
759
+
760
+ ```typescript
761
+ function setLogger(logger: Logger): void
762
+
763
+ interface Logger {
764
+ info(message: string, ...args: any[]): void;
765
+ error(message: string, ...args: any[]): void;
766
+ warn(message: string, ...args: any[]): void;
767
+ verbose(message: string, ...args: any[]): void;
768
+ debug(message: string, ...args: any[]): void;
769
+ }
770
+ ```
74
771
 
75
- ## Types
772
+ **Parameters:**
773
+ - `logger` - Logger implementation matching the Logger interface
774
+
775
+ **Example:**
776
+ ```typescript
777
+ setLogger({
778
+ info: (msg) => console.log(`[INFO] ${msg}`),
779
+ error: (msg) => console.error(`[ERROR] ${msg}`),
780
+ warn: (msg) => console.warn(`[WARN] ${msg}`),
781
+ verbose: (msg) => process.env.VERBOSE && console.log(`[VERBOSE] ${msg}`),
782
+ debug: (msg) => process.env.DEBUG && console.log(`[DEBUG] ${msg}`)
783
+ });
784
+ ```
785
+
786
+ ## TypeScript Types
787
+
788
+ ### PackageInfo
76
789
 
77
790
  ```typescript
78
791
  interface PackageInfo {
79
- name: string;
80
- version: string;
81
- path: string;
82
- dependencies: Set<string>;
83
- devDependencies: Set<string>;
84
- localDependencies: Set<string>;
792
+ name: string; // Package name from package.json
793
+ version: string; // Semantic version
794
+ path: string; // Absolute path to package directory
795
+ dependencies: Set<string>; // All dependencies
796
+ devDependencies: Set<string>; // Dev dependencies only
797
+ localDependencies: Set<string>; // Workspace packages only
85
798
  }
799
+ ```
800
+
801
+ ### DependencyGraph
86
802
 
803
+ ```typescript
87
804
  interface DependencyGraph {
88
- packages: Map<string, PackageInfo>;
89
- edges: Map<string, Set<string>>;
90
- reverseEdges: Map<string, Set<string>>;
805
+ packages: Map<string, PackageInfo>; // Package name -> info
806
+ edges: Map<string, Set<string>>; // Package -> dependencies
807
+ reverseEdges: Map<string, Set<string>>; // Package -> dependents
808
+ }
809
+ ```
810
+
811
+ ### SerializedGraph
812
+
813
+ ```typescript
814
+ interface SerializedGraph {
815
+ packages: Array<{
816
+ name: string;
817
+ version: string;
818
+ path: string;
819
+ dependencies: string[];
820
+ }>;
821
+ edges: Array<[string, string[]]>;
91
822
  }
92
823
  ```
93
824
 
825
+ ## Common Patterns
826
+
827
+ ### Incremental Builds
828
+
829
+ Only rebuild affected packages:
830
+
831
+ ```typescript
832
+ import { findAllDependents, topologicalSort } from '@eldrforge/tree-core';
833
+
834
+ async function incrementalBuild(
835
+ graph: DependencyGraph,
836
+ changedPackages: string[]
837
+ ) {
838
+ // Find all affected packages
839
+ const affected = new Set(changedPackages);
840
+ for (const changed of changedPackages) {
841
+ const dependents = findAllDependents(changed, graph);
842
+ dependents.forEach(dep => affected.add(dep));
843
+ }
844
+
845
+ // Get full build order
846
+ const fullOrder = topologicalSort(graph);
847
+
848
+ // Filter to only affected packages, preserving order
849
+ const buildOrder = fullOrder.filter(pkg => affected.has(pkg));
850
+
851
+ console.log(`Building ${buildOrder.length} affected packages`);
852
+ return buildOrder;
853
+ }
854
+ ```
855
+
856
+ ### Workspace Validation
857
+
858
+ Validate workspace before operations:
859
+
860
+ ```typescript
861
+ async function validateWorkspace(workspaceRoot: string): Promise<boolean> {
862
+ try {
863
+ const packages = await scanForPackageJsonFiles(workspaceRoot);
864
+
865
+ if (packages.length === 0) {
866
+ console.error('No packages found in workspace');
867
+ return false;
868
+ }
869
+
870
+ const graph = await buildDependencyGraph(packages);
871
+ const validation = validateGraph(graph);
872
+
873
+ if (!validation.valid) {
874
+ console.error('Workspace validation failed:');
875
+ validation.errors.forEach(err => console.error(` - ${err}`));
876
+ return false;
877
+ }
878
+
879
+ console.log(`āœ… Workspace valid: ${packages.length} packages`);
880
+ return true;
881
+ } catch (error) {
882
+ console.error('Validation error:', error);
883
+ return false;
884
+ }
885
+ }
886
+ ```
887
+
888
+ ### Dependency Visualization
889
+
890
+ Generate visualization data:
891
+
892
+ ```typescript
893
+ function generateGraphviz(graph: DependencyGraph): string {
894
+ let dot = 'digraph dependencies {\n';
895
+ dot += ' rankdir=LR;\n';
896
+ dot += ' node [shape=box];\n\n';
897
+
898
+ for (const [pkg, deps] of graph.edges) {
899
+ for (const dep of deps) {
900
+ dot += ` "${pkg}" -> "${dep}";\n`;
901
+ }
902
+ }
903
+
904
+ dot += '}\n';
905
+ return dot;
906
+ }
907
+
908
+ // Save to file and visualize with graphviz:
909
+ // dot -Tpng deps.dot > deps.png
910
+ ```
911
+
912
+ ## Error Handling
913
+
914
+ All async functions may throw errors. Recommended error handling:
915
+
916
+ ```typescript
917
+ import {
918
+ scanForPackageJsonFiles,
919
+ buildDependencyGraph,
920
+ topologicalSort
921
+ } from '@eldrforge/tree-core';
922
+
923
+ async function safeBuildOrder(workspaceRoot: string): Promise<string[] | null> {
924
+ try {
925
+ const packages = await scanForPackageJsonFiles(workspaceRoot);
926
+ const graph = await buildDependencyGraph(packages);
927
+ const buildOrder = topologicalSort(graph);
928
+ return buildOrder;
929
+ } catch (error) {
930
+ if (error.message.includes('Circular dependency')) {
931
+ console.error('āŒ Circular dependency detected:', error.message);
932
+ console.error(' Fix the circular dependency and try again');
933
+ } else if (error.message.includes('no name field')) {
934
+ console.error('āŒ Invalid package.json:', error.message);
935
+ console.error(' Ensure all package.json files have a "name" field');
936
+ } else if (error.code === 'ENOENT') {
937
+ console.error('āŒ Directory not found:', error.path);
938
+ } else {
939
+ console.error('āŒ Unexpected error:', error.message);
940
+ }
941
+ return null;
942
+ }
943
+ }
944
+ ```
945
+
946
+ ## Performance Considerations
947
+
948
+ - **Caching**: Use `serializeGraph`/`deserializeGraph` to cache graph builds
949
+ - **Exclusion Patterns**: Always exclude `node_modules`, `dist`, and other large directories
950
+ - **Parallel Processing**: Use the parallel build pattern for better performance
951
+ - **Incremental Updates**: Only rebuild affected packages when possible
952
+
953
+ ## Test Coverage
954
+
955
+ The library maintains **94.11% test coverage** with comprehensive unit and integration tests:
956
+
957
+ ```
958
+ File | % Stmts | % Branch | % Funcs | % Lines
959
+ -------------------|---------|----------|---------|--------
960
+ dependencyGraph.ts | 94.11 | 90.76 | 80 | 94.11
961
+ ```
962
+
94
963
  ## Dependencies
95
964
 
96
- - `@eldrforge/git-tools` - Git operations and validation
97
- - `@eldrforge/shared` - Shared utilities
965
+ This library depends on:
966
+
967
+ - `@eldrforge/git-tools` - Git validation and JSON parsing utilities
968
+ - `@eldrforge/shared` - Storage and filesystem abstractions
969
+
970
+ ## Development
971
+
972
+ ### Building
973
+
974
+ ```bash
975
+ npm run build
976
+ ```
977
+
978
+ ### Testing
979
+
980
+ ```bash
981
+ # Run tests
982
+ npm test
983
+
984
+ # Watch mode
985
+ npm run test:watch
986
+
987
+ # With coverage
988
+ npm run test:coverage
989
+ ```
990
+
991
+ ### Linting
992
+
993
+ ```bash
994
+ npm run lint
995
+ ```
996
+
997
+ ## Use Cases
998
+
999
+ This library is ideal for:
1000
+
1001
+ - **Monorepo Build Systems** - Determine optimal build order
1002
+ - **CI/CD Pipelines** - Incremental builds based on changes
1003
+ - **Package Publishing** - Publish in dependency order
1004
+ - **Workspace Analysis** - Understand package relationships
1005
+ - **Refactoring Tools** - Find impact of changes
1006
+ - **Documentation Generation** - Create dependency diagrams
1007
+ - **Testing Frameworks** - Run tests in correct order
1008
+ - **Migration Scripts** - Update packages in safe order
1009
+
1010
+ ## Real-World Example: Build System
1011
+
1012
+ Complete example of a build system using tree-core:
1013
+
1014
+ ```typescript
1015
+ import {
1016
+ scanForPackageJsonFiles,
1017
+ buildDependencyGraph,
1018
+ topologicalSort,
1019
+ validateGraph,
1020
+ findAllDependents,
1021
+ setLogger
1022
+ } from '@eldrforge/tree-core';
1023
+ import { execSync } from 'child_process';
1024
+ import path from 'path';
1025
+
1026
+ // Configure logging
1027
+ setLogger({
1028
+ info: console.log,
1029
+ error: console.error,
1030
+ warn: console.warn,
1031
+ verbose: () => {}, // Disable verbose
1032
+ debug: () => {} // Disable debug
1033
+ });
1034
+
1035
+ async function buildWorkspace(
1036
+ workspaceRoot: string,
1037
+ options: {
1038
+ incremental?: boolean;
1039
+ changedPackages?: string[];
1040
+ parallel?: boolean;
1041
+ } = {}
1042
+ ) {
1043
+ console.log('šŸ” Scanning workspace...');
1044
+ const packages = await scanForPackageJsonFiles(workspaceRoot, [
1045
+ '**/node_modules/**',
1046
+ '**/dist/**'
1047
+ ]);
1048
+
1049
+ console.log(`šŸ“¦ Found ${packages.length} packages`);
1050
+
1051
+ console.log('šŸ“Š Building dependency graph...');
1052
+ const graph = await buildDependencyGraph(packages);
1053
+
1054
+ console.log('āœ… Validating graph...');
1055
+ const validation = validateGraph(graph);
1056
+ if (!validation.valid) {
1057
+ console.error('āŒ Validation failed:');
1058
+ validation.errors.forEach(err => console.error(` ${err}`));
1059
+ process.exit(1);
1060
+ }
1061
+
1062
+ // Determine what to build
1063
+ let buildOrder = topologicalSort(graph);
1064
+
1065
+ if (options.incremental && options.changedPackages) {
1066
+ console.log(`šŸ”„ Incremental build for ${options.changedPackages.length} changed packages`);
1067
+ const affected = new Set(options.changedPackages);
1068
+ for (const changed of options.changedPackages) {
1069
+ const dependents = findAllDependents(changed, graph);
1070
+ dependents.forEach(dep => affected.add(dep));
1071
+ }
1072
+ buildOrder = buildOrder.filter(pkg => affected.has(pkg));
1073
+ console.log(` Building ${buildOrder.length} affected packages`);
1074
+ }
1075
+
1076
+ // Build packages
1077
+ console.log('šŸ”Ø Starting build...\n');
1078
+ let succeeded = 0;
1079
+ let failed = 0;
1080
+
1081
+ for (const pkgName of buildOrder) {
1082
+ const pkg = graph.packages.get(pkgName)!;
1083
+ const pkgDir = pkg.path;
1084
+
1085
+ console.log(`šŸ“¦ Building ${pkgName}...`);
1086
+
1087
+ try {
1088
+ execSync('npm run build', {
1089
+ cwd: pkgDir,
1090
+ stdio: 'pipe',
1091
+ encoding: 'utf-8'
1092
+ });
1093
+ console.log(` āœ… ${pkgName} built successfully\n`);
1094
+ succeeded++;
1095
+ } catch (error) {
1096
+ console.error(` āŒ ${pkgName} build failed`);
1097
+ console.error(` ${error.message}\n`);
1098
+ failed++;
1099
+
1100
+ if (!options.incremental) {
1101
+ console.error('šŸ’„ Build failed, stopping...');
1102
+ process.exit(1);
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ console.log('\nšŸ“Š Build Summary');
1108
+ console.log(` Total: ${buildOrder.length} packages`);
1109
+ console.log(` āœ… Succeeded: ${succeeded}`);
1110
+ console.log(` āŒ Failed: ${failed}`);
1111
+
1112
+ return { succeeded, failed };
1113
+ }
1114
+
1115
+ // Run it
1116
+ const args = process.argv.slice(2);
1117
+ const workspaceRoot = args[0] || process.cwd();
1118
+
1119
+ buildWorkspace(workspaceRoot, {
1120
+ incremental: process.env.INCREMENTAL === 'true',
1121
+ changedPackages: process.env.CHANGED?.split(','),
1122
+ parallel: false
1123
+ })
1124
+ .then(({ succeeded, failed }) => {
1125
+ process.exit(failed > 0 ? 1 : 0);
1126
+ })
1127
+ .catch(error => {
1128
+ console.error('Fatal error:', error);
1129
+ process.exit(1);
1130
+ });
1131
+ ```
1132
+
1133
+ Save as `build.ts` and run:
1134
+
1135
+ ```bash
1136
+ # Full build
1137
+ npx tsx build.ts /path/to/workspace
1138
+
1139
+ # Incremental build
1140
+ INCREMENTAL=true CHANGED="@myorg/core,@myorg/utils" npx tsx build.ts /path/to/workspace
1141
+ ```
1142
+
1143
+ ## Contributing
1144
+
1145
+ Contributions are welcome! Please ensure:
1146
+
1147
+ 1. All tests pass (`npm test`)
1148
+ 2. Code coverage remains above 90% (`npm run test:coverage`)
1149
+ 3. Code passes linting (`npm run lint`)
1150
+ 4. TypeScript compiles without errors (`npm run build`)
98
1151
 
99
1152
  ## License
100
1153
 
101
1154
  MIT Ā© Calen Varek
102
1155
 
1156
+ ## Links
1157
+
1158
+ - [GitHub Repository](https://github.com/calenvarek/tree-core)
1159
+ - [Issue Tracker](https://github.com/calenvarek/tree-core/issues)
1160
+ - [npm Package](https://www.npmjs.com/package/@eldrforge/tree-core)
1161
+
1162
+ ## Support
1163
+
1164
+ For questions, issues, or feature requests, please [open an issue](https://github.com/calenvarek/tree-core/issues) on GitHub.
1165
+
1166
+ ---
1167
+
1168
+ Made with ā¤ļø for the monorepo community
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eldrforge/tree-core",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Dependency graph algorithms for monorepo package analysis",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -33,8 +33,8 @@
33
33
  },
34
34
  "homepage": "https://github.com/calenvarek/tree-core#readme",
35
35
  "dependencies": {
36
- "@eldrforge/git-tools": "^0.1.13",
37
- "@eldrforge/shared": "^0.1.2"
36
+ "@eldrforge/git-tools": "^0.1.14",
37
+ "@eldrforge/shared": "^0.1.3"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@eslint/eslintrc": "^3.3.1",