@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,416 @@
1
+ /**
2
+ * Analysis orchestrator for workspace analysis.
3
+ *
4
+ * Adapts the doc-sync sync-orchestrator pattern for workspace analysis,
5
+ * coordinating scanner, analyzers, and reporters into a unified pipeline.
6
+ */
7
+
8
+ import type {Analyzer, AnalyzerError} from '../analyzers/analyzer'
9
+ import type {MergedConfig} from '../config/merger'
10
+ import type {WorkspacePackage} from '../scanner/workspace-scanner'
11
+ import type {
12
+ AnalysisProgress,
13
+ AnalysisResult,
14
+ AnalysisSummary,
15
+ Issue,
16
+ Severity,
17
+ } from '../types/index'
18
+ import type {Result} from '../types/result'
19
+
20
+ import path from 'node:path'
21
+
22
+ import {pLimit} from '@bfra.me/es/async'
23
+ import {consola} from 'consola'
24
+
25
+ import {createDefaultRegistry} from '../analyzers/index'
26
+ import {getAnalyzerOptions} from '../config/merger'
27
+ import {createWorkspaceScanner} from '../scanner/workspace-scanner'
28
+ import {err, ok} from '../types/result'
29
+
30
+ /**
31
+ * Extended analysis context shared between analyzers.
32
+ * Contains all information needed to perform analysis operations.
33
+ */
34
+ export interface AnalysisContext {
35
+ /** Root path of the workspace being analyzed */
36
+ readonly workspacePath: string
37
+ /** All packages discovered in the workspace */
38
+ readonly packages: readonly WorkspacePackage[]
39
+ /** All source files in the workspace */
40
+ readonly sourceFiles: readonly string[]
41
+ /** Merged configuration */
42
+ readonly config: MergedConfig
43
+ /** Configuration hash for caching */
44
+ readonly configHash: string
45
+ /** Report progress during analysis */
46
+ readonly reportProgress: (message: string) => void
47
+ }
48
+
49
+ /**
50
+ * Error codes for orchestration operations.
51
+ */
52
+ export type OrchestratorErrorCode =
53
+ | 'SCAN_FAILED'
54
+ | 'ANALYSIS_FAILED'
55
+ | 'INVALID_CONFIG'
56
+ | 'NO_PACKAGES'
57
+
58
+ /**
59
+ * Error that occurred during orchestration.
60
+ */
61
+ export interface OrchestratorError {
62
+ readonly code: OrchestratorErrorCode
63
+ readonly message: string
64
+ readonly cause?: unknown
65
+ }
66
+
67
+ /**
68
+ * Options for the analysis orchestrator.
69
+ */
70
+ export interface OrchestratorOptions {
71
+ /** Root path of the workspace to analyze */
72
+ readonly workspacePath: string
73
+ /** Merged configuration */
74
+ readonly config: MergedConfig
75
+ /** Progress callback */
76
+ readonly onProgress?: (progress: AnalysisProgress) => void
77
+ /** Verbose logging */
78
+ readonly verbose?: boolean
79
+ }
80
+
81
+ /**
82
+ * Analysis orchestrator interface.
83
+ */
84
+ export interface AnalysisOrchestrator {
85
+ /** Run full analysis on the workspace */
86
+ readonly analyzeAll: () => Promise<Result<AnalysisResult, OrchestratorError>>
87
+ /** Run analysis on specific packages */
88
+ readonly analyzePackages: (
89
+ packageNames: readonly string[],
90
+ ) => Promise<Result<AnalysisResult, OrchestratorError>>
91
+ /** Get the current analysis context */
92
+ readonly getContext: () => Promise<Result<AnalysisContext, OrchestratorError>>
93
+ }
94
+
95
+ /**
96
+ * Severity ordering for filtering.
97
+ */
98
+ const SEVERITY_ORDER: Record<Severity, number> = {
99
+ info: 0,
100
+ warning: 1,
101
+ error: 2,
102
+ critical: 3,
103
+ }
104
+
105
+ /**
106
+ * Computes a simple hash from configuration for cache invalidation.
107
+ */
108
+ function computeConfigHash(config: MergedConfig): string {
109
+ const configStr = JSON.stringify({
110
+ include: config.include,
111
+ exclude: config.exclude,
112
+ categories: config.categories,
113
+ rules: config.rules,
114
+ analyzers: config.analyzers,
115
+ architecture: config.architecture,
116
+ })
117
+
118
+ let hash = 0
119
+ for (let i = 0; i < configStr.length; i++) {
120
+ const char = configStr.charCodeAt(i)
121
+ hash = (hash << 5) - hash + char
122
+ hash = hash & hash
123
+ }
124
+ return Math.abs(hash).toString(16)
125
+ }
126
+
127
+ /**
128
+ * Creates an analysis summary from issues.
129
+ */
130
+ function createSummary(
131
+ issues: readonly Issue[],
132
+ packagesAnalyzed: number,
133
+ filesAnalyzed: number,
134
+ durationMs: number,
135
+ ): AnalysisSummary {
136
+ const bySeverity: Record<Severity, number> = {
137
+ info: 0,
138
+ warning: 0,
139
+ error: 0,
140
+ critical: 0,
141
+ }
142
+
143
+ const byCategory: Record<string, number> = {
144
+ configuration: 0,
145
+ dependency: 0,
146
+ architecture: 0,
147
+ performance: 0,
148
+ 'circular-import': 0,
149
+ 'unused-export': 0,
150
+ 'type-safety': 0,
151
+ }
152
+
153
+ for (const issue of issues) {
154
+ bySeverity[issue.severity]++
155
+ byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1
156
+ }
157
+
158
+ return {
159
+ totalIssues: issues.length,
160
+ bySeverity,
161
+ byCategory: byCategory as AnalysisSummary['byCategory'],
162
+ packagesAnalyzed,
163
+ filesAnalyzed,
164
+ durationMs,
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Filters issues by minimum severity threshold.
170
+ */
171
+ function filterBySeverity(issues: readonly Issue[], minSeverity: Severity): readonly Issue[] {
172
+ const minLevel = SEVERITY_ORDER[minSeverity]
173
+ return issues.filter(issue => SEVERITY_ORDER[issue.severity] >= minLevel)
174
+ }
175
+
176
+ /**
177
+ * Creates an analysis orchestrator for coordinating workspace analysis.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * const orchestrator = createOrchestrator({
182
+ * config: mergedConfig,
183
+ * onProgress: (progress) => console.log(progress.phase),
184
+ * })
185
+ *
186
+ * const result = await orchestrator.analyzeAll()
187
+ * if (result.success) {
188
+ * console.log(`Found ${result.data.summary.totalIssues} issues`)
189
+ * }
190
+ * ```
191
+ */
192
+ export function createOrchestrator(options: OrchestratorOptions): AnalysisOrchestrator {
193
+ const {workspacePath, config, onProgress, verbose = false} = options
194
+
195
+ const scanner = createWorkspaceScanner({
196
+ rootDir: workspacePath,
197
+ includePatterns: config.packagePatterns,
198
+ excludePackages: [],
199
+ sourceExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'],
200
+ excludeDirs: ['node_modules', 'dist', 'lib', 'build', '__tests__', '__mocks__'],
201
+ })
202
+
203
+ const registry = createDefaultRegistry()
204
+ const limit = pLimit(config.concurrency)
205
+
206
+ function log(message: string): void {
207
+ if (verbose) {
208
+ consola.info(message)
209
+ }
210
+ }
211
+
212
+ function reportProgress(
213
+ phase: AnalysisProgress['phase'],
214
+ current: string,
215
+ processed: number,
216
+ total?: number,
217
+ ): void {
218
+ onProgress?.({phase, current, processed, total})
219
+ }
220
+
221
+ async function buildContext(
222
+ workspacePath: string,
223
+ ): Promise<Result<AnalysisContext, OrchestratorError>> {
224
+ log('Scanning workspace...')
225
+ reportProgress('scanning', workspacePath, 0)
226
+
227
+ const scanResult = await scanner.scan()
228
+
229
+ if (scanResult.errors.length > 0) {
230
+ const errorMessages = scanResult.errors.map(e => e.message).join('; ')
231
+ consola.warn(`Scan completed with ${scanResult.errors.length} errors: ${errorMessages}`)
232
+ }
233
+
234
+ if (scanResult.packages.length === 0) {
235
+ return err({
236
+ code: 'NO_PACKAGES',
237
+ message: 'No packages found in workspace',
238
+ })
239
+ }
240
+
241
+ log(`Found ${scanResult.packages.length} packages`)
242
+ reportProgress(
243
+ 'scanning',
244
+ workspacePath,
245
+ scanResult.packages.length,
246
+ scanResult.packages.length,
247
+ )
248
+
249
+ const allSourceFiles = scanResult.packages.flatMap(pkg => [...pkg.sourceFiles])
250
+
251
+ const configHash = computeConfigHash(config)
252
+
253
+ const context: AnalysisContext = {
254
+ workspacePath: scanResult.workspacePath,
255
+ packages: scanResult.packages,
256
+ sourceFiles: allSourceFiles,
257
+ config,
258
+ configHash,
259
+ reportProgress: log,
260
+ }
261
+
262
+ return ok(context)
263
+ }
264
+
265
+ async function runAnalyzers(
266
+ context: AnalysisContext,
267
+ packages: readonly WorkspacePackage[],
268
+ ): Promise<readonly Issue[]> {
269
+ const allIssues: Issue[] = []
270
+ const enabledAnalyzers: Analyzer[] = []
271
+
272
+ // Filter to enabled analyzers based on config
273
+ for (const analyzer of registry.getEnabled()) {
274
+ const analyzerOpts = getAnalyzerOptions(config, analyzer.metadata.id)
275
+ if (analyzerOpts.enabled) {
276
+ enabledAnalyzers.push(analyzer)
277
+ }
278
+ }
279
+
280
+ log(`Running ${enabledAnalyzers.length} analyzers...`)
281
+ reportProgress('analyzing', '', 0, enabledAnalyzers.length)
282
+
283
+ // Run analyzers in parallel with concurrency limit
284
+ const results = await Promise.all(
285
+ enabledAnalyzers.map(async (analyzer, index) =>
286
+ limit(async (): Promise<Result<readonly Issue[], AnalyzerError>> => {
287
+ const analyzerId = analyzer.metadata.id
288
+ reportProgress('analyzing', analyzerId, index + 1, enabledAnalyzers.length)
289
+ log(`Running analyzer: ${analyzerId}`)
290
+
291
+ try {
292
+ const analyzerContext = {
293
+ workspacePath: context.workspacePath,
294
+ packages,
295
+ config: {
296
+ minSeverity: context.config.minSeverity,
297
+ categories: context.config.categories,
298
+ include: context.config.include,
299
+ exclude: context.config.exclude,
300
+ rules: context.config.rules,
301
+ },
302
+ reportProgress: context.reportProgress,
303
+ }
304
+
305
+ const result = await analyzer.analyze(analyzerContext)
306
+ return result
307
+ } catch (error) {
308
+ consola.warn(`Analyzer ${analyzerId} failed: ${(error as Error).message}`)
309
+ return err({
310
+ code: 'ANALYZER_ERROR',
311
+ message: `Analyzer ${analyzerId} failed: ${(error as Error).message}`,
312
+ })
313
+ }
314
+ }),
315
+ ),
316
+ )
317
+
318
+ // Collect all issues
319
+ for (const result of results) {
320
+ if (result.success) {
321
+ allIssues.push(...result.data)
322
+ }
323
+ }
324
+
325
+ // Filter by minimum severity
326
+ const filteredIssues = filterBySeverity(allIssues, config.minSeverity)
327
+
328
+ log(`Found ${filteredIssues.length} issues after filtering`)
329
+
330
+ return filteredIssues
331
+ }
332
+
333
+ return {
334
+ async analyzeAll(): Promise<Result<AnalysisResult, OrchestratorError>> {
335
+ const startTime = Date.now()
336
+ const workspacePath = path.resolve('.')
337
+
338
+ const contextResult = await buildContext(workspacePath)
339
+ if (!contextResult.success) {
340
+ return contextResult
341
+ }
342
+
343
+ const context = contextResult.data
344
+ const issues = await runAnalyzers(context, context.packages)
345
+
346
+ const durationMs = Date.now() - startTime
347
+ reportProgress('reporting', '', context.packages.length, context.packages.length)
348
+
349
+ const summary = createSummary(
350
+ issues,
351
+ context.packages.length,
352
+ context.sourceFiles.length,
353
+ durationMs,
354
+ )
355
+
356
+ log(`Analysis complete in ${durationMs}ms`)
357
+
358
+ return ok({
359
+ issues,
360
+ summary,
361
+ workspacePath: context.workspacePath,
362
+ startedAt: new Date(startTime),
363
+ completedAt: new Date(),
364
+ })
365
+ },
366
+
367
+ async analyzePackages(
368
+ packageNames: readonly string[],
369
+ ): Promise<Result<AnalysisResult, OrchestratorError>> {
370
+ const startTime = Date.now()
371
+
372
+ const contextResult = await buildContext(workspacePath)
373
+ if (!contextResult.success) {
374
+ return contextResult
375
+ }
376
+
377
+ const context = contextResult.data
378
+
379
+ // Filter to requested packages
380
+ const targetPackages = context.packages.filter(pkg => packageNames.includes(pkg.name))
381
+
382
+ if (targetPackages.length === 0) {
383
+ return err({
384
+ code: 'NO_PACKAGES',
385
+ message: `No packages found matching: ${packageNames.join(', ')}`,
386
+ })
387
+ }
388
+
389
+ log(
390
+ `Analyzing ${targetPackages.length} packages: ${targetPackages.map(p => p.name).join(', ')}`,
391
+ )
392
+
393
+ const issues = await runAnalyzers(context, targetPackages)
394
+
395
+ const durationMs = Date.now() - startTime
396
+ reportProgress('reporting', '', targetPackages.length, targetPackages.length)
397
+
398
+ const filesAnalyzed = targetPackages.reduce((sum, pkg) => sum + pkg.sourceFiles.length, 0)
399
+
400
+ const summary = createSummary(issues, targetPackages.length, filesAnalyzed, durationMs)
401
+
402
+ return ok({
403
+ issues,
404
+ summary,
405
+ workspacePath: context.workspacePath,
406
+ startedAt: new Date(startTime),
407
+ completedAt: new Date(),
408
+ })
409
+ },
410
+
411
+ async getContext(): Promise<Result<AnalysisContext, OrchestratorError>> {
412
+ const workspacePath = path.resolve('.')
413
+ return buildContext(workspacePath)
414
+ },
415
+ }
416
+ }