@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,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
|
+
}
|