@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.
- package/README.md +1112 -46
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
# @eldrforge/tree-core
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
File | % Stmts | % Branch | % Funcs | % Lines
|
|
11
|
-
-------------------|---------|----------|---------|--------
|
|
12
|
-
dependencyGraph.ts | 94.11 | 90.76 | 80 | 94.11
|
|
13
|
-
```
|
|
5
|
+
[](./coverage)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
14
9
|
|
|
15
10
|
## Features
|
|
16
11
|
|
|
17
|
-
- š¦ **Package Discovery** -
|
|
18
|
-
- š **Dependency Analysis** - Build dependency graphs
|
|
19
|
-
- š **Topological Sort** - Determine build order
|
|
20
|
-
- ā ļø **Circular Detection** - Identify circular dependencies
|
|
21
|
-
- šÆ **Pattern Filtering** - Exclude packages
|
|
22
|
-
-
|
|
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
|
-
|
|
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
|
|
40
|
-
const packagePaths = await scanForPackageJsonFiles('/path/to/
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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.
|
|
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.
|
|
37
|
-
"@eldrforge/shared": "^0.1.
|
|
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",
|