@bfra.me/workspace-analyzer 0.1.0
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 +402 -0
- package/lib/chunk-4LSFAAZW.js +1 -0
- package/lib/chunk-JDF7DQ4V.js +27 -0
- package/lib/chunk-WOJ4C7N7.js +7122 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +318 -0
- package/lib/index.d.ts +3701 -0
- package/lib/index.js +1262 -0
- package/lib/types/index.d.ts +146 -0
- package/lib/types/index.js +28 -0
- package/package.json +89 -0
- package/src/analyzers/analyzer.ts +201 -0
- package/src/analyzers/architectural-analyzer.ts +304 -0
- package/src/analyzers/build-config-analyzer.ts +334 -0
- package/src/analyzers/circular-import-analyzer.ts +463 -0
- package/src/analyzers/config-consistency-analyzer.ts +335 -0
- package/src/analyzers/dead-code-analyzer.ts +565 -0
- package/src/analyzers/duplicate-code-analyzer.ts +626 -0
- package/src/analyzers/duplicate-dependency-analyzer.ts +381 -0
- package/src/analyzers/eslint-config-analyzer.ts +281 -0
- package/src/analyzers/exports-field-analyzer.ts +324 -0
- package/src/analyzers/index.ts +388 -0
- package/src/analyzers/large-dependency-analyzer.ts +535 -0
- package/src/analyzers/package-json-analyzer.ts +349 -0
- package/src/analyzers/peer-dependency-analyzer.ts +275 -0
- package/src/analyzers/tree-shaking-analyzer.ts +623 -0
- package/src/analyzers/tsconfig-analyzer.ts +382 -0
- package/src/analyzers/unused-dependency-analyzer.ts +356 -0
- package/src/analyzers/version-alignment-analyzer.ts +308 -0
- package/src/api/analyze-workspace.ts +245 -0
- package/src/api/index.ts +11 -0
- package/src/cache/cache-manager.ts +495 -0
- package/src/cache/cache-schema.ts +247 -0
- package/src/cache/change-detector.ts +169 -0
- package/src/cache/file-hasher.ts +65 -0
- package/src/cache/index.ts +47 -0
- package/src/cli/commands/analyze.ts +240 -0
- package/src/cli/commands/index.ts +5 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/types.ts +65 -0
- package/src/cli/ui.ts +213 -0
- package/src/cli.ts +9 -0
- package/src/config/defaults.ts +183 -0
- package/src/config/index.ts +81 -0
- package/src/config/loader.ts +270 -0
- package/src/config/merger.ts +229 -0
- package/src/config/schema.ts +263 -0
- package/src/core/incremental-analyzer.ts +462 -0
- package/src/core/index.ts +34 -0
- package/src/core/orchestrator.ts +416 -0
- package/src/graph/dependency-graph.ts +408 -0
- package/src/graph/index.ts +19 -0
- package/src/index.ts +417 -0
- package/src/parser/config-parser.ts +491 -0
- package/src/parser/import-extractor.ts +340 -0
- package/src/parser/index.ts +54 -0
- package/src/parser/typescript-parser.ts +95 -0
- package/src/performance/bundle-estimator.ts +444 -0
- package/src/performance/index.ts +27 -0
- package/src/reporters/console-reporter.ts +355 -0
- package/src/reporters/index.ts +49 -0
- package/src/reporters/json-reporter.ts +273 -0
- package/src/reporters/markdown-reporter.ts +349 -0
- package/src/reporters/reporter.ts +399 -0
- package/src/rules/builtin-rules.ts +709 -0
- package/src/rules/index.ts +52 -0
- package/src/rules/rule-engine.ts +409 -0
- package/src/scanner/index.ts +18 -0
- package/src/scanner/workspace-scanner.ts +403 -0
- package/src/types/index.ts +176 -0
- package/src/types/result.ts +19 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/pattern-matcher.ts +48 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency graph builder for tracking import relationships.
|
|
3
|
+
*
|
|
4
|
+
* Builds an adjacency list representation of module dependencies
|
|
5
|
+
* for cycle detection and dependency analysis using Tarjan's algorithm.
|
|
6
|
+
*
|
|
7
|
+
* This module uses types from import-extractor.ts which depends on ts-morph
|
|
8
|
+
* (provided as a peer dependency via @bfra.me/doc-sync).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {ExtractedImport, ImportExtractionResult} from '../parser/import-extractor'
|
|
12
|
+
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A node in the dependency graph representing a module.
|
|
17
|
+
*/
|
|
18
|
+
export interface DependencyNode {
|
|
19
|
+
/** Unique identifier (typically the file path) */
|
|
20
|
+
readonly id: string
|
|
21
|
+
/** Display name for the node */
|
|
22
|
+
readonly name: string
|
|
23
|
+
/** File path of the module */
|
|
24
|
+
readonly filePath: string
|
|
25
|
+
/** Package name if this is the root of a package */
|
|
26
|
+
readonly packageName?: string
|
|
27
|
+
/** Outgoing edges (modules this node imports from) */
|
|
28
|
+
readonly imports: readonly string[]
|
|
29
|
+
/** Incoming edges (modules that import this node) */
|
|
30
|
+
readonly importedBy: readonly string[]
|
|
31
|
+
/** All import details for this node */
|
|
32
|
+
readonly importDetails: readonly ExtractedImport[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An edge in the dependency graph representing an import relationship.
|
|
37
|
+
*/
|
|
38
|
+
export interface DependencyEdge {
|
|
39
|
+
/** Source node (the importing module) */
|
|
40
|
+
readonly from: string
|
|
41
|
+
/** Target node (the imported module) */
|
|
42
|
+
readonly to: string
|
|
43
|
+
/** Import type */
|
|
44
|
+
readonly type: ExtractedImport['type']
|
|
45
|
+
/** Whether this is a type-only import */
|
|
46
|
+
readonly isTypeOnly: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A cycle detected in the dependency graph.
|
|
51
|
+
*/
|
|
52
|
+
export interface DependencyCycle {
|
|
53
|
+
/** Nodes in the cycle, in order */
|
|
54
|
+
readonly nodes: readonly string[]
|
|
55
|
+
/** Edges forming the cycle */
|
|
56
|
+
readonly edges: readonly DependencyEdge[]
|
|
57
|
+
/** Human-readable representation of the cycle */
|
|
58
|
+
readonly description: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The complete dependency graph.
|
|
63
|
+
*/
|
|
64
|
+
export interface DependencyGraph {
|
|
65
|
+
/** All nodes in the graph */
|
|
66
|
+
readonly nodes: ReadonlyMap<string, DependencyNode>
|
|
67
|
+
/** All edges in the graph */
|
|
68
|
+
readonly edges: readonly DependencyEdge[]
|
|
69
|
+
/** Root workspace path */
|
|
70
|
+
readonly rootPath: string
|
|
71
|
+
/** Total number of modules */
|
|
72
|
+
readonly moduleCount: number
|
|
73
|
+
/** Total number of edges (import relationships) */
|
|
74
|
+
readonly edgeCount: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Statistics about the dependency graph.
|
|
79
|
+
*/
|
|
80
|
+
export interface GraphStatistics {
|
|
81
|
+
/** Total number of nodes */
|
|
82
|
+
readonly nodeCount: number
|
|
83
|
+
/** Total number of edges */
|
|
84
|
+
readonly edgeCount: number
|
|
85
|
+
/** Number of external dependencies */
|
|
86
|
+
readonly externalDependencies: number
|
|
87
|
+
/** Number of workspace dependencies */
|
|
88
|
+
readonly workspaceDependencies: number
|
|
89
|
+
/** Most imported modules (by incoming edge count) */
|
|
90
|
+
readonly mostImported: readonly {readonly id: string; readonly count: number}[]
|
|
91
|
+
/** Modules with most imports (by outgoing edge count) */
|
|
92
|
+
readonly mostImporting: readonly {readonly id: string; readonly count: number}[]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Options for building a dependency graph.
|
|
97
|
+
*/
|
|
98
|
+
export interface DependencyGraphOptions {
|
|
99
|
+
/** Root path of the workspace */
|
|
100
|
+
readonly rootPath: string
|
|
101
|
+
/** Include type-only imports in the graph */
|
|
102
|
+
readonly includeTypeImports?: boolean
|
|
103
|
+
/** Resolve relative imports to absolute paths */
|
|
104
|
+
readonly resolveRelativeImports?: boolean
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Builds a dependency graph from import extraction results.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const results: ImportExtractionResult[] = [...]
|
|
113
|
+
* const graph = buildDependencyGraph(results, {rootPath: '/workspace'})
|
|
114
|
+
*
|
|
115
|
+
* for (const [id, node] of graph.nodes) {
|
|
116
|
+
* console.log(`${node.name} imports ${node.imports.length} modules`)
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function buildDependencyGraph(
|
|
121
|
+
results: readonly ImportExtractionResult[],
|
|
122
|
+
options: DependencyGraphOptions,
|
|
123
|
+
): DependencyGraph {
|
|
124
|
+
const {rootPath, includeTypeImports = true, resolveRelativeImports = true} = options
|
|
125
|
+
|
|
126
|
+
const nodes = new Map<string, DependencyNode>()
|
|
127
|
+
const edges: DependencyEdge[] = []
|
|
128
|
+
const importedByMap = new Map<string, string[]>()
|
|
129
|
+
|
|
130
|
+
// First pass: create nodes for all files
|
|
131
|
+
for (const result of results) {
|
|
132
|
+
const nodeId = normalizeNodeId(result.filePath, rootPath)
|
|
133
|
+
const imports: string[] = []
|
|
134
|
+
const importDetails: ExtractedImport[] = []
|
|
135
|
+
|
|
136
|
+
for (const imp of result.imports) {
|
|
137
|
+
if (imp.type === 'type-only' && !includeTypeImports) {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let targetId: string
|
|
142
|
+
if (imp.isRelative && resolveRelativeImports) {
|
|
143
|
+
targetId = resolveRelativeImportPath(result.filePath, imp.moduleSpecifier)
|
|
144
|
+
targetId = normalizeNodeId(targetId, rootPath)
|
|
145
|
+
} else {
|
|
146
|
+
targetId = imp.moduleSpecifier
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!imports.includes(targetId)) {
|
|
150
|
+
imports.push(targetId)
|
|
151
|
+
}
|
|
152
|
+
importDetails.push(imp)
|
|
153
|
+
|
|
154
|
+
edges.push({
|
|
155
|
+
from: nodeId,
|
|
156
|
+
to: targetId,
|
|
157
|
+
type: imp.type,
|
|
158
|
+
isTypeOnly: imp.type === 'type-only',
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Track reverse edges
|
|
162
|
+
const importedBy = importedByMap.get(targetId)
|
|
163
|
+
if (importedBy === undefined) {
|
|
164
|
+
importedByMap.set(targetId, [nodeId])
|
|
165
|
+
} else if (!importedBy.includes(nodeId)) {
|
|
166
|
+
importedBy.push(nodeId)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
nodes.set(nodeId, {
|
|
171
|
+
id: nodeId,
|
|
172
|
+
name: getNodeDisplayName(result.filePath, rootPath),
|
|
173
|
+
filePath: result.filePath,
|
|
174
|
+
imports,
|
|
175
|
+
importedBy: [],
|
|
176
|
+
importDetails,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Second pass: populate importedBy references
|
|
181
|
+
for (const [nodeId, node] of nodes) {
|
|
182
|
+
const importedBy = importedByMap.get(nodeId) ?? []
|
|
183
|
+
nodes.set(nodeId, {
|
|
184
|
+
...node,
|
|
185
|
+
importedBy,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
nodes,
|
|
191
|
+
edges,
|
|
192
|
+
rootPath,
|
|
193
|
+
moduleCount: nodes.size,
|
|
194
|
+
edgeCount: edges.length,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Finds all cycles in a dependency graph using Tarjan's algorithm.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```ts
|
|
203
|
+
* const cycles = findCycles(graph)
|
|
204
|
+
* for (const cycle of cycles) {
|
|
205
|
+
* console.log(`Cycle detected: ${cycle.description}`)
|
|
206
|
+
* }
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export function findCycles(graph: DependencyGraph): readonly DependencyCycle[] {
|
|
210
|
+
const cycles: DependencyCycle[] = []
|
|
211
|
+
const visited = new Set<string>()
|
|
212
|
+
const recursionStack = new Set<string>()
|
|
213
|
+
const path: string[] = []
|
|
214
|
+
|
|
215
|
+
function dfs(nodeId: string): void {
|
|
216
|
+
if (recursionStack.has(nodeId)) {
|
|
217
|
+
// Found a cycle
|
|
218
|
+
const cycleStart = path.indexOf(nodeId)
|
|
219
|
+
if (cycleStart !== -1) {
|
|
220
|
+
const cycleNodes = path.slice(cycleStart)
|
|
221
|
+
cycleNodes.push(nodeId)
|
|
222
|
+
|
|
223
|
+
const cycleEdges: DependencyEdge[] = []
|
|
224
|
+
for (let i = 0; i < cycleNodes.length - 1; i++) {
|
|
225
|
+
const from = cycleNodes[i]
|
|
226
|
+
const to = cycleNodes[i + 1]
|
|
227
|
+
const edge = graph.edges.find(e => e.from === from && e.to === to)
|
|
228
|
+
if (edge !== undefined) {
|
|
229
|
+
cycleEdges.push(edge)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
cycles.push({
|
|
234
|
+
nodes: cycleNodes.slice(0, -1),
|
|
235
|
+
edges: cycleEdges,
|
|
236
|
+
description: cycleNodes.join(' → '),
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (visited.has(nodeId)) {
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
visited.add(nodeId)
|
|
247
|
+
recursionStack.add(nodeId)
|
|
248
|
+
path.push(nodeId)
|
|
249
|
+
|
|
250
|
+
const node = graph.nodes.get(nodeId)
|
|
251
|
+
if (node !== undefined) {
|
|
252
|
+
for (const importId of node.imports) {
|
|
253
|
+
dfs(importId)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
path.pop()
|
|
258
|
+
recursionStack.delete(nodeId)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const nodeId of graph.nodes.keys()) {
|
|
262
|
+
if (!visited.has(nodeId)) {
|
|
263
|
+
dfs(nodeId)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return cycles
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Computes statistics about the dependency graph.
|
|
272
|
+
*/
|
|
273
|
+
export function computeGraphStatistics(graph: DependencyGraph, topN = 10): GraphStatistics {
|
|
274
|
+
let externalDependencies = 0
|
|
275
|
+
let workspaceDependencies = 0
|
|
276
|
+
const importCounts: {id: string; count: number}[] = []
|
|
277
|
+
const importedByCounts: {id: string; count: number}[] = []
|
|
278
|
+
|
|
279
|
+
for (const [id, node] of graph.nodes) {
|
|
280
|
+
importCounts.push({id, count: node.imports.length})
|
|
281
|
+
importedByCounts.push({id, count: node.importedBy.length})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Count external and workspace imports from edges
|
|
285
|
+
const seenExternal = new Set<string>()
|
|
286
|
+
const seenWorkspace = new Set<string>()
|
|
287
|
+
|
|
288
|
+
for (const edge of graph.edges) {
|
|
289
|
+
if (!graph.nodes.has(edge.to)) {
|
|
290
|
+
// Target is external
|
|
291
|
+
if (edge.to.startsWith('@')) {
|
|
292
|
+
if (!seenWorkspace.has(edge.to)) {
|
|
293
|
+
workspaceDependencies++
|
|
294
|
+
seenWorkspace.add(edge.to)
|
|
295
|
+
}
|
|
296
|
+
} else if (!seenExternal.has(edge.to)) {
|
|
297
|
+
externalDependencies++
|
|
298
|
+
seenExternal.add(edge.to)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Sort by count descending
|
|
304
|
+
importCounts.sort((a, b) => b.count - a.count)
|
|
305
|
+
importedByCounts.sort((a, b) => b.count - a.count)
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
nodeCount: graph.nodes.size,
|
|
309
|
+
edgeCount: graph.edges.length,
|
|
310
|
+
externalDependencies,
|
|
311
|
+
workspaceDependencies,
|
|
312
|
+
mostImported: importedByCounts.slice(0, topN),
|
|
313
|
+
mostImporting: importCounts.slice(0, topN),
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Gets all transitive dependencies of a node.
|
|
319
|
+
*/
|
|
320
|
+
export function getTransitiveDependencies(
|
|
321
|
+
graph: DependencyGraph,
|
|
322
|
+
nodeId: string,
|
|
323
|
+
): readonly string[] {
|
|
324
|
+
const dependencies = new Set<string>()
|
|
325
|
+
const visited = new Set<string>()
|
|
326
|
+
|
|
327
|
+
function traverse(id: string): void {
|
|
328
|
+
if (visited.has(id)) {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
visited.add(id)
|
|
332
|
+
|
|
333
|
+
const node = graph.nodes.get(id)
|
|
334
|
+
if (node !== undefined) {
|
|
335
|
+
for (const importId of node.imports) {
|
|
336
|
+
dependencies.add(importId)
|
|
337
|
+
traverse(importId)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
traverse(nodeId)
|
|
343
|
+
dependencies.delete(nodeId)
|
|
344
|
+
|
|
345
|
+
return [...dependencies]
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Gets all modules that transitively depend on a node.
|
|
350
|
+
*/
|
|
351
|
+
export function getTransitiveDependents(graph: DependencyGraph, nodeId: string): readonly string[] {
|
|
352
|
+
const dependents = new Set<string>()
|
|
353
|
+
const visited = new Set<string>()
|
|
354
|
+
|
|
355
|
+
function traverse(id: string): void {
|
|
356
|
+
if (visited.has(id)) {
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
visited.add(id)
|
|
360
|
+
|
|
361
|
+
const node = graph.nodes.get(id)
|
|
362
|
+
if (node !== undefined) {
|
|
363
|
+
for (const dependentId of node.importedBy) {
|
|
364
|
+
dependents.add(dependentId)
|
|
365
|
+
traverse(dependentId)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
traverse(nodeId)
|
|
371
|
+
dependents.delete(nodeId)
|
|
372
|
+
|
|
373
|
+
return [...dependents]
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Normalizes a file path to a node ID.
|
|
378
|
+
*/
|
|
379
|
+
function normalizeNodeId(filePath: string, rootPath: string): string {
|
|
380
|
+
if (filePath.startsWith(rootPath)) {
|
|
381
|
+
return filePath.slice(rootPath.length).replace(/^\//, '')
|
|
382
|
+
}
|
|
383
|
+
return filePath
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Gets a display name for a node from its file path.
|
|
388
|
+
*/
|
|
389
|
+
function getNodeDisplayName(filePath: string, rootPath: string): string {
|
|
390
|
+
const relativePath = normalizeNodeId(filePath, rootPath)
|
|
391
|
+
return relativePath
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Resolves a relative import path to an absolute path.
|
|
396
|
+
*/
|
|
397
|
+
function resolveRelativeImportPath(fromFile: string, moduleSpecifier: string): string {
|
|
398
|
+
const fromDir = path.dirname(fromFile)
|
|
399
|
+
let resolved = path.resolve(fromDir, moduleSpecifier)
|
|
400
|
+
|
|
401
|
+
const hasExtension = /\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)$/.test(resolved)
|
|
402
|
+
if (!hasExtension) {
|
|
403
|
+
// Default to .ts extension for TypeScript projects
|
|
404
|
+
resolved = `${resolved}.ts`
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return resolved
|
|
408
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency graph module exports.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
buildDependencyGraph,
|
|
7
|
+
computeGraphStatistics,
|
|
8
|
+
findCycles,
|
|
9
|
+
getTransitiveDependencies,
|
|
10
|
+
getTransitiveDependents,
|
|
11
|
+
} from './dependency-graph'
|
|
12
|
+
export type {
|
|
13
|
+
DependencyCycle,
|
|
14
|
+
DependencyEdge,
|
|
15
|
+
DependencyGraph,
|
|
16
|
+
DependencyGraphOptions,
|
|
17
|
+
DependencyNode,
|
|
18
|
+
GraphStatistics,
|
|
19
|
+
} from './dependency-graph'
|