@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.
Files changed (73) hide show
  1. package/README.md +402 -0
  2. package/lib/chunk-4LSFAAZW.js +1 -0
  3. package/lib/chunk-JDF7DQ4V.js +27 -0
  4. package/lib/chunk-WOJ4C7N7.js +7122 -0
  5. package/lib/cli.d.ts +1 -0
  6. package/lib/cli.js +318 -0
  7. package/lib/index.d.ts +3701 -0
  8. package/lib/index.js +1262 -0
  9. package/lib/types/index.d.ts +146 -0
  10. package/lib/types/index.js +28 -0
  11. package/package.json +89 -0
  12. package/src/analyzers/analyzer.ts +201 -0
  13. package/src/analyzers/architectural-analyzer.ts +304 -0
  14. package/src/analyzers/build-config-analyzer.ts +334 -0
  15. package/src/analyzers/circular-import-analyzer.ts +463 -0
  16. package/src/analyzers/config-consistency-analyzer.ts +335 -0
  17. package/src/analyzers/dead-code-analyzer.ts +565 -0
  18. package/src/analyzers/duplicate-code-analyzer.ts +626 -0
  19. package/src/analyzers/duplicate-dependency-analyzer.ts +381 -0
  20. package/src/analyzers/eslint-config-analyzer.ts +281 -0
  21. package/src/analyzers/exports-field-analyzer.ts +324 -0
  22. package/src/analyzers/index.ts +388 -0
  23. package/src/analyzers/large-dependency-analyzer.ts +535 -0
  24. package/src/analyzers/package-json-analyzer.ts +349 -0
  25. package/src/analyzers/peer-dependency-analyzer.ts +275 -0
  26. package/src/analyzers/tree-shaking-analyzer.ts +623 -0
  27. package/src/analyzers/tsconfig-analyzer.ts +382 -0
  28. package/src/analyzers/unused-dependency-analyzer.ts +356 -0
  29. package/src/analyzers/version-alignment-analyzer.ts +308 -0
  30. package/src/api/analyze-workspace.ts +245 -0
  31. package/src/api/index.ts +11 -0
  32. package/src/cache/cache-manager.ts +495 -0
  33. package/src/cache/cache-schema.ts +247 -0
  34. package/src/cache/change-detector.ts +169 -0
  35. package/src/cache/file-hasher.ts +65 -0
  36. package/src/cache/index.ts +47 -0
  37. package/src/cli/commands/analyze.ts +240 -0
  38. package/src/cli/commands/index.ts +5 -0
  39. package/src/cli/index.ts +61 -0
  40. package/src/cli/types.ts +65 -0
  41. package/src/cli/ui.ts +213 -0
  42. package/src/cli.ts +9 -0
  43. package/src/config/defaults.ts +183 -0
  44. package/src/config/index.ts +81 -0
  45. package/src/config/loader.ts +270 -0
  46. package/src/config/merger.ts +229 -0
  47. package/src/config/schema.ts +263 -0
  48. package/src/core/incremental-analyzer.ts +462 -0
  49. package/src/core/index.ts +34 -0
  50. package/src/core/orchestrator.ts +416 -0
  51. package/src/graph/dependency-graph.ts +408 -0
  52. package/src/graph/index.ts +19 -0
  53. package/src/index.ts +417 -0
  54. package/src/parser/config-parser.ts +491 -0
  55. package/src/parser/import-extractor.ts +340 -0
  56. package/src/parser/index.ts +54 -0
  57. package/src/parser/typescript-parser.ts +95 -0
  58. package/src/performance/bundle-estimator.ts +444 -0
  59. package/src/performance/index.ts +27 -0
  60. package/src/reporters/console-reporter.ts +355 -0
  61. package/src/reporters/index.ts +49 -0
  62. package/src/reporters/json-reporter.ts +273 -0
  63. package/src/reporters/markdown-reporter.ts +349 -0
  64. package/src/reporters/reporter.ts +399 -0
  65. package/src/rules/builtin-rules.ts +709 -0
  66. package/src/rules/index.ts +52 -0
  67. package/src/rules/rule-engine.ts +409 -0
  68. package/src/scanner/index.ts +18 -0
  69. package/src/scanner/workspace-scanner.ts +403 -0
  70. package/src/types/index.ts +176 -0
  71. package/src/types/result.ts +19 -0
  72. package/src/utils/index.ts +7 -0
  73. 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'