@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,399 @@
1
+ /**
2
+ * Report generator interface and base types for workspace analysis output.
3
+ *
4
+ * Defines the contract for reporters that format analysis results into
5
+ * various output formats (JSON, Markdown, console, etc.).
6
+ */
7
+
8
+ import type {AnalysisResult, Issue, IssueCategory, Severity} from '../types/index'
9
+
10
+ /**
11
+ * Supported output formats for analysis reports.
12
+ */
13
+ export type ReportFormat = 'json' | 'markdown' | 'console'
14
+
15
+ /**
16
+ * Options for configuring report generation.
17
+ */
18
+ export interface ReportOptions {
19
+ /** Minimum severity level to include in the report */
20
+ readonly minSeverity?: Severity
21
+ /** Categories of issues to include (empty means all) */
22
+ readonly categories?: readonly IssueCategory[]
23
+ /** Whether to include file location details (line/column numbers) */
24
+ readonly includeLocation?: boolean
25
+ /** Whether to include fix suggestions */
26
+ readonly includeSuggestions?: boolean
27
+ /** Whether to group issues by file, category, or severity */
28
+ readonly groupBy?: 'file' | 'category' | 'severity' | 'none'
29
+ /** Whether to include summary statistics */
30
+ readonly includeSummary?: boolean
31
+ /** Output file path (if writing to file) */
32
+ readonly outputPath?: string
33
+ /** Whether to use colors in output (for console reporter) */
34
+ readonly colors?: boolean
35
+ /** Maximum number of issues to display per group */
36
+ readonly maxIssuesPerGroup?: number
37
+ /** Whether to include metadata in output */
38
+ readonly includeMetadata?: boolean
39
+ }
40
+
41
+ /**
42
+ * Grouped issues organized by a specific key.
43
+ */
44
+ export interface GroupedIssues {
45
+ /** The grouping key (file path, category name, or severity level) */
46
+ readonly key: string
47
+ /** Human-readable label for the group */
48
+ readonly label: string
49
+ /** Issues in this group */
50
+ readonly issues: readonly Issue[]
51
+ /** Number of issues in this group */
52
+ readonly count: number
53
+ }
54
+
55
+ /**
56
+ * Summary statistics for report output.
57
+ */
58
+ export interface ReportSummary {
59
+ /** Total number of issues */
60
+ readonly totalIssues: number
61
+ /** Issues by severity */
62
+ readonly bySeverity: Readonly<Record<Severity, number>>
63
+ /** Issues by category */
64
+ readonly byCategory: Readonly<Record<IssueCategory, number>>
65
+ /** Number of packages analyzed */
66
+ readonly packagesAnalyzed: number
67
+ /** Number of files analyzed */
68
+ readonly filesAnalyzed: number
69
+ /** Duration in milliseconds */
70
+ readonly durationMs: number
71
+ /** Number of files with issues */
72
+ readonly filesWithIssues: number
73
+ /** Highest severity level found */
74
+ readonly highestSeverity: Severity | null
75
+ }
76
+
77
+ /**
78
+ * Reporter interface that all report generators must implement.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const jsonReporter = createJsonReporter()
83
+ * const report = jsonReporter.generate(analysisResult)
84
+ * await fs.writeFile('report.json', report)
85
+ * ```
86
+ */
87
+ export interface Reporter {
88
+ /** Unique identifier for this reporter */
89
+ readonly id: ReportFormat
90
+ /** Human-readable name for this reporter */
91
+ readonly name: string
92
+ /** Generate a report string from analysis results */
93
+ readonly generate: (result: AnalysisResult, options?: ReportOptions) => string
94
+ /** Stream report output to a writable stream (for large reports) */
95
+ readonly stream?: (
96
+ result: AnalysisResult,
97
+ write: (chunk: string) => void,
98
+ options?: ReportOptions,
99
+ ) => void
100
+ }
101
+
102
+ /**
103
+ * Factory function type for creating reporter instances.
104
+ */
105
+ export type ReporterFactory = (options?: ReportOptions) => Reporter
106
+
107
+ /**
108
+ * Default report options.
109
+ */
110
+ export const DEFAULT_REPORT_OPTIONS: Required<
111
+ Omit<ReportOptions, 'outputPath' | 'categories' | 'minSeverity'>
112
+ > = {
113
+ includeLocation: true,
114
+ includeSuggestions: true,
115
+ groupBy: 'file',
116
+ includeSummary: true,
117
+ colors: true,
118
+ maxIssuesPerGroup: 50,
119
+ includeMetadata: false,
120
+ }
121
+
122
+ /**
123
+ * Severity display configuration for consistent formatting.
124
+ */
125
+ export const SEVERITY_CONFIG: Readonly<
126
+ Record<
127
+ Severity,
128
+ {
129
+ label: string
130
+ emoji: string
131
+ color: string
132
+ priority: number
133
+ }
134
+ >
135
+ > = {
136
+ critical: {label: 'Critical', emoji: '🚨', color: 'red', priority: 4},
137
+ error: {label: 'Error', emoji: '❌', color: 'red', priority: 3},
138
+ warning: {label: 'Warning', emoji: '⚠️', color: 'yellow', priority: 2},
139
+ info: {label: 'Info', emoji: 'ℹ️', color: 'blue', priority: 1},
140
+ }
141
+
142
+ /**
143
+ * Category display configuration for consistent formatting.
144
+ */
145
+ export const CATEGORY_CONFIG: Readonly<
146
+ Record<
147
+ IssueCategory,
148
+ {
149
+ label: string
150
+ emoji: string
151
+ description: string
152
+ }
153
+ >
154
+ > = {
155
+ configuration: {
156
+ label: 'Configuration',
157
+ emoji: '⚙️',
158
+ description: 'Package and project configuration issues',
159
+ },
160
+ dependency: {
161
+ label: 'Dependency',
162
+ emoji: '📦',
163
+ description: 'Package dependency issues',
164
+ },
165
+ architecture: {
166
+ label: 'Architecture',
167
+ emoji: '🏗️',
168
+ description: 'Architectural pattern violations',
169
+ },
170
+ performance: {
171
+ label: 'Performance',
172
+ emoji: '🚀',
173
+ description: 'Performance optimization opportunities',
174
+ },
175
+ 'circular-import': {
176
+ label: 'Circular Import',
177
+ emoji: '🔄',
178
+ description: 'Circular import chains',
179
+ },
180
+ 'unused-export': {
181
+ label: 'Unused Export',
182
+ emoji: '📤',
183
+ description: 'Exported but unused code',
184
+ },
185
+ 'type-safety': {
186
+ label: 'Type Safety',
187
+ emoji: '🔒',
188
+ description: 'Type system issues',
189
+ },
190
+ }
191
+
192
+ /**
193
+ * Filter issues based on report options.
194
+ */
195
+ export function filterIssuesForReport(
196
+ issues: readonly Issue[],
197
+ options: ReportOptions,
198
+ ): readonly Issue[] {
199
+ const severityOrder: Record<Severity, number> = {
200
+ info: 0,
201
+ warning: 1,
202
+ error: 2,
203
+ critical: 3,
204
+ }
205
+
206
+ return issues.filter(issue => {
207
+ if (
208
+ options.minSeverity !== undefined &&
209
+ severityOrder[issue.severity] < severityOrder[options.minSeverity]
210
+ ) {
211
+ return false
212
+ }
213
+
214
+ if (
215
+ options.categories !== undefined &&
216
+ options.categories.length > 0 &&
217
+ !options.categories.includes(issue.category)
218
+ ) {
219
+ return false
220
+ }
221
+
222
+ return true
223
+ })
224
+ }
225
+
226
+ /**
227
+ * Group issues by the specified key.
228
+ */
229
+ export function groupIssues(
230
+ issues: readonly Issue[],
231
+ groupBy: ReportOptions['groupBy'],
232
+ ): readonly GroupedIssues[] {
233
+ if (groupBy === 'none' || groupBy === undefined) {
234
+ return [{key: 'all', label: 'All Issues', issues, count: issues.length}]
235
+ }
236
+
237
+ const groups = new Map<string, Issue[]>()
238
+
239
+ for (const issue of issues) {
240
+ let key: string
241
+ switch (groupBy) {
242
+ case 'file':
243
+ key = issue.location.filePath
244
+ break
245
+ case 'category':
246
+ key = issue.category
247
+ break
248
+ case 'severity':
249
+ key = issue.severity
250
+ break
251
+ }
252
+
253
+ const existing = groups.get(key) ?? []
254
+ existing.push(issue)
255
+ groups.set(key, existing)
256
+ }
257
+
258
+ const result: GroupedIssues[] = []
259
+ for (const [key, groupIssues] of groups) {
260
+ let label: string
261
+ switch (groupBy) {
262
+ case 'file':
263
+ label = key
264
+ break
265
+ case 'category':
266
+ label = CATEGORY_CONFIG[key as IssueCategory]?.label ?? key
267
+ break
268
+ case 'severity':
269
+ label = SEVERITY_CONFIG[key as Severity]?.label ?? key
270
+ break
271
+ }
272
+
273
+ result.push({key, label, issues: groupIssues, count: groupIssues.length})
274
+ }
275
+
276
+ if (groupBy === 'severity') {
277
+ result.sort((a, b) => {
278
+ const priorityA = SEVERITY_CONFIG[a.key as Severity]?.priority ?? 0
279
+ const priorityB = SEVERITY_CONFIG[b.key as Severity]?.priority ?? 0
280
+ return priorityB - priorityA
281
+ })
282
+ } else if (groupBy === 'category') {
283
+ result.sort((a, b) => a.label.localeCompare(b.label))
284
+ } else {
285
+ result.sort((a, b) => b.count - a.count)
286
+ }
287
+
288
+ return result
289
+ }
290
+
291
+ /**
292
+ * Calculate summary statistics from analysis results.
293
+ */
294
+ export function calculateSummary(result: AnalysisResult): ReportSummary {
295
+ const bySeverity: Record<Severity, number> = {
296
+ info: 0,
297
+ warning: 0,
298
+ error: 0,
299
+ critical: 0,
300
+ }
301
+
302
+ const byCategory: Record<IssueCategory, number> = {
303
+ configuration: 0,
304
+ dependency: 0,
305
+ architecture: 0,
306
+ performance: 0,
307
+ 'circular-import': 0,
308
+ 'unused-export': 0,
309
+ 'type-safety': 0,
310
+ }
311
+
312
+ const filesWithIssues = new Set<string>()
313
+ let highestSeverity: Severity | null = null
314
+ const severityOrder: Record<Severity, number> = {
315
+ info: 0,
316
+ warning: 1,
317
+ error: 2,
318
+ critical: 3,
319
+ }
320
+
321
+ for (const issue of result.issues) {
322
+ bySeverity[issue.severity]++
323
+ byCategory[issue.category]++
324
+ filesWithIssues.add(issue.location.filePath)
325
+
326
+ if (
327
+ highestSeverity === null ||
328
+ severityOrder[issue.severity] > severityOrder[highestSeverity]
329
+ ) {
330
+ highestSeverity = issue.severity
331
+ }
332
+ }
333
+
334
+ return {
335
+ totalIssues: result.summary.totalIssues,
336
+ bySeverity,
337
+ byCategory,
338
+ packagesAnalyzed: result.summary.packagesAnalyzed,
339
+ filesAnalyzed: result.summary.filesAnalyzed,
340
+ durationMs: result.summary.durationMs,
341
+ filesWithIssues: filesWithIssues.size,
342
+ highestSeverity,
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Format a file location for display.
348
+ */
349
+ export function formatLocation(
350
+ location: Issue['location'],
351
+ options?: {includeColumn?: boolean},
352
+ ): string {
353
+ let result = location.filePath
354
+
355
+ if (location.line !== undefined) {
356
+ result += `:${location.line}`
357
+ if (options?.includeColumn && location.column !== undefined) {
358
+ result += `:${location.column}`
359
+ }
360
+ }
361
+
362
+ return result
363
+ }
364
+
365
+ /**
366
+ * Format duration for human-readable display.
367
+ */
368
+ export function formatDuration(ms: number): string {
369
+ if (ms < 1000) {
370
+ return `${ms}ms`
371
+ }
372
+ if (ms < 60000) {
373
+ return `${(ms / 1000).toFixed(2)}s`
374
+ }
375
+ const minutes = Math.floor(ms / 60000)
376
+ const seconds = ((ms % 60000) / 1000).toFixed(0)
377
+ return `${minutes}m ${seconds}s`
378
+ }
379
+
380
+ /**
381
+ * Truncate text to a maximum length with ellipsis.
382
+ */
383
+ export function truncateText(text: string, maxLength: number): string {
384
+ if (text.length <= maxLength) {
385
+ return text
386
+ }
387
+ return `${text.slice(0, maxLength - 3)}...`
388
+ }
389
+
390
+ /**
391
+ * Get relative path from workspace root.
392
+ */
393
+ export function getRelativePath(filePath: string, workspacePath: string): string {
394
+ if (filePath.startsWith(workspacePath)) {
395
+ const relative = filePath.slice(workspacePath.length)
396
+ return relative.startsWith('/') ? relative.slice(1) : relative
397
+ }
398
+ return filePath
399
+ }