@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,349 @@
1
+ /**
2
+ * Markdown reporter for human-readable analysis output.
3
+ *
4
+ * Generates well-formatted Markdown reports suitable for GitHub issues,
5
+ * pull request comments, documentation, and team review.
6
+ */
7
+
8
+ import type {AnalysisResult, Issue, Severity} from '../types/index'
9
+ import type {GroupedIssues, Reporter, ReportOptions, ReportSummary} from './reporter'
10
+
11
+ import {
12
+ calculateSummary,
13
+ CATEGORY_CONFIG,
14
+ DEFAULT_REPORT_OPTIONS,
15
+ filterIssuesForReport,
16
+ formatDuration,
17
+ getRelativePath,
18
+ groupIssues,
19
+ SEVERITY_CONFIG,
20
+ } from './reporter'
21
+
22
+ /**
23
+ * Options specific to Markdown reporter.
24
+ */
25
+ export interface MarkdownReporterOptions extends ReportOptions {
26
+ /** Whether to use GitHub-flavored Markdown features (checkboxes, alerts) */
27
+ readonly githubFlavored?: boolean
28
+ /** Title for the report document */
29
+ readonly title?: string
30
+ /** Whether to include table of contents */
31
+ readonly includeTableOfContents?: boolean
32
+ /** Whether to use emoji in headers and status */
33
+ readonly useEmoji?: boolean
34
+ /** Whether to use collapsible sections for issue details */
35
+ readonly collapsibleDetails?: boolean
36
+ /** Maximum issues to show per group before truncating */
37
+ readonly maxIssuesPerGroup?: number
38
+ }
39
+
40
+ /**
41
+ * Create a Markdown reporter instance.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const reporter = createMarkdownReporter({githubFlavored: true})
46
+ * const markdown = reporter.generate(analysisResult)
47
+ * await fs.writeFile('report.md', markdown)
48
+ * ```
49
+ */
50
+ export function createMarkdownReporter(defaultOptions?: MarkdownReporterOptions): Reporter {
51
+ return {
52
+ id: 'markdown',
53
+ name: 'Markdown Reporter',
54
+ generate(result: AnalysisResult, options?: ReportOptions): string {
55
+ const mergedOptions: MarkdownReporterOptions = {
56
+ ...DEFAULT_REPORT_OPTIONS,
57
+ ...defaultOptions,
58
+ ...options,
59
+ }
60
+
61
+ return generateMarkdownReport(result, mergedOptions)
62
+ },
63
+ stream(result: AnalysisResult, write: (chunk: string) => void, options?: ReportOptions): void {
64
+ const output = this.generate(result, options)
65
+ write(output)
66
+ },
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Generate the complete Markdown report.
72
+ */
73
+ function generateMarkdownReport(result: AnalysisResult, options: MarkdownReporterOptions): string {
74
+ const lines: string[] = []
75
+ const filteredIssues = filterIssuesForReport(result.issues, options)
76
+ const summary = calculateSummary({...result, issues: filteredIssues})
77
+ const grouped = groupIssues(filteredIssues, options.groupBy)
78
+ const useEmoji = options.useEmoji ?? true
79
+
80
+ lines.push(generateTitle(options, summary, useEmoji))
81
+ lines.push('')
82
+
83
+ if (options.githubFlavored) {
84
+ lines.push(generateGitHubAlert(summary))
85
+ lines.push('')
86
+ }
87
+
88
+ if (options.includeTableOfContents && grouped.length > 1) {
89
+ lines.push(generateTableOfContents(grouped, options))
90
+ lines.push('')
91
+ }
92
+
93
+ if (options.includeSummary) {
94
+ lines.push(generateSummarySection(summary, result, options, useEmoji))
95
+ lines.push('')
96
+ }
97
+
98
+ lines.push(generateIssuesSection(grouped, result.workspacePath, options, useEmoji))
99
+
100
+ return lines.join('\n')
101
+ }
102
+
103
+ /**
104
+ * Generate report title.
105
+ */
106
+ function generateTitle(
107
+ options: MarkdownReporterOptions,
108
+ summary: ReportSummary,
109
+ useEmoji: boolean,
110
+ ): string {
111
+ const title = options.title ?? 'Workspace Analysis Report'
112
+ const statusEmoji = getStatusEmoji(summary.highestSeverity, useEmoji)
113
+ return `# ${statusEmoji} ${title}`
114
+ }
115
+
116
+ /**
117
+ * Generate GitHub alert box based on severity.
118
+ */
119
+ function generateGitHubAlert(summary: ReportSummary): string {
120
+ if (summary.totalIssues === 0) {
121
+ return '> [!TIP]\n> No issues found! Your workspace is in great shape.'
122
+ }
123
+
124
+ if (summary.highestSeverity === 'critical' || summary.highestSeverity === 'error') {
125
+ return `> [!CAUTION]\n> Found ${summary.totalIssues} issue(s) requiring attention.`
126
+ }
127
+
128
+ if (summary.highestSeverity === 'warning') {
129
+ return `> [!WARNING]\n> Found ${summary.totalIssues} issue(s) to review.`
130
+ }
131
+
132
+ return `> [!NOTE]\n> Found ${summary.totalIssues} informational issue(s).`
133
+ }
134
+
135
+ /**
136
+ * Generate table of contents.
137
+ */
138
+ function generateTableOfContents(
139
+ groups: readonly GroupedIssues[],
140
+ options: MarkdownReporterOptions,
141
+ ): string {
142
+ const lines: string[] = ['## Table of Contents', '']
143
+
144
+ if (options.includeSummary) {
145
+ lines.push('- [Summary](#summary)')
146
+ }
147
+
148
+ for (const group of groups) {
149
+ const anchor = group.label.toLowerCase().replaceAll(/[^a-z0-9]+/g, '-')
150
+ lines.push(`- [${group.label}](#${anchor}) (${group.count})`)
151
+ }
152
+
153
+ return lines.join('\n')
154
+ }
155
+
156
+ /**
157
+ * Generate summary statistics section.
158
+ */
159
+ function generateSummarySection(
160
+ summary: ReportSummary,
161
+ _result: AnalysisResult,
162
+ _options: MarkdownReporterOptions,
163
+ useEmoji: boolean,
164
+ ): string {
165
+ const lines: string[] = ['## Summary', '']
166
+
167
+ lines.push('### Overview', '')
168
+ lines.push(`| Metric | Value |`)
169
+ lines.push(`|--------|-------|`)
170
+ lines.push(`| Total Issues | ${summary.totalIssues} |`)
171
+ lines.push(`| Packages Analyzed | ${summary.packagesAnalyzed} |`)
172
+ lines.push(`| Files Analyzed | ${summary.filesAnalyzed} |`)
173
+ lines.push(`| Files with Issues | ${summary.filesWithIssues} |`)
174
+ lines.push(`| Duration | ${formatDuration(summary.durationMs)} |`)
175
+ lines.push('')
176
+
177
+ lines.push('### Issues by Severity', '')
178
+ lines.push(`| Severity | Count |`)
179
+ lines.push(`|----------|-------|`)
180
+ for (const severity of ['critical', 'error', 'warning', 'info'] as const) {
181
+ const count = summary.bySeverity[severity]
182
+ const config = SEVERITY_CONFIG[severity]
183
+ const emoji = useEmoji ? `${config.emoji} ` : ''
184
+ lines.push(`| ${emoji}${config.label} | ${count} |`)
185
+ }
186
+ lines.push('')
187
+
188
+ lines.push('### Issues by Category', '')
189
+ lines.push(`| Category | Count |`)
190
+ lines.push(`|----------|-------|`)
191
+ for (const [category, count] of Object.entries(summary.byCategory)) {
192
+ if (count > 0) {
193
+ const config = CATEGORY_CONFIG[category as keyof typeof CATEGORY_CONFIG]
194
+ const emoji = useEmoji ? `${config.emoji} ` : ''
195
+ lines.push(`| ${emoji}${config.label} | ${count} |`)
196
+ }
197
+ }
198
+
199
+ return lines.join('\n')
200
+ }
201
+
202
+ /**
203
+ * Generate issues section with grouping.
204
+ */
205
+ function generateIssuesSection(
206
+ groups: readonly GroupedIssues[],
207
+ workspacePath: string,
208
+ options: MarkdownReporterOptions,
209
+ useEmoji: boolean,
210
+ ): string {
211
+ const lines: string[] = ['## Issues', '']
212
+
213
+ if (groups.length === 0 || groups.every(g => g.count === 0)) {
214
+ lines.push(useEmoji ? '✅ No issues found!' : 'No issues found!')
215
+ return lines.join('\n')
216
+ }
217
+
218
+ for (const group of groups) {
219
+ if (group.count === 0) {
220
+ continue
221
+ }
222
+
223
+ lines.push(generateGroupHeader(group, options, useEmoji))
224
+ lines.push('')
225
+
226
+ const maxIssues = options.maxIssuesPerGroup ?? 50
227
+ const issuesToShow = group.issues.slice(0, maxIssues)
228
+ const remainingCount = group.count - issuesToShow.length
229
+
230
+ for (const issue of issuesToShow) {
231
+ lines.push(formatIssueMarkdown(issue, workspacePath, options, useEmoji))
232
+ lines.push('')
233
+ }
234
+
235
+ if (remainingCount > 0) {
236
+ lines.push(`*...and ${remainingCount} more issue(s)*`)
237
+ lines.push('')
238
+ }
239
+ }
240
+
241
+ return lines.join('\n')
242
+ }
243
+
244
+ /**
245
+ * Generate group header.
246
+ */
247
+ function generateGroupHeader(
248
+ group: GroupedIssues,
249
+ options: MarkdownReporterOptions,
250
+ useEmoji: boolean,
251
+ ): string {
252
+ let emoji = ''
253
+ if (useEmoji) {
254
+ if (options.groupBy === 'severity') {
255
+ const severityEmoji = SEVERITY_CONFIG[group.key as Severity]?.emoji
256
+ emoji = severityEmoji === undefined ? '' : `${severityEmoji} `
257
+ } else if (options.groupBy === 'category') {
258
+ const categoryEmoji = CATEGORY_CONFIG[group.key as keyof typeof CATEGORY_CONFIG]?.emoji
259
+ emoji = categoryEmoji === undefined ? '' : `${categoryEmoji} `
260
+ } else {
261
+ emoji = '📄 '
262
+ }
263
+ }
264
+
265
+ return `### ${emoji}${group.label} (${group.count})`
266
+ }
267
+
268
+ /**
269
+ * Format a single issue as Markdown.
270
+ */
271
+ function formatIssueMarkdown(
272
+ issue: Issue,
273
+ workspacePath: string,
274
+ options: MarkdownReporterOptions,
275
+ useEmoji: boolean,
276
+ ): string {
277
+ const lines: string[] = []
278
+ const severityConfig = SEVERITY_CONFIG[issue.severity]
279
+ const emoji = useEmoji ? `${severityConfig.emoji} ` : ''
280
+
281
+ const severityBadge = `\`${severityConfig.label.toUpperCase()}\``
282
+ lines.push(`#### ${emoji}${issue.title}`)
283
+ lines.push('')
284
+ lines.push(
285
+ `**Severity:** ${severityBadge} | **Category:** ${CATEGORY_CONFIG[issue.category].label}`,
286
+ )
287
+ lines.push('')
288
+
289
+ if (options.includeLocation) {
290
+ const relativePath = getRelativePath(issue.location.filePath, workspacePath)
291
+ let location = `📍 \`${relativePath}\``
292
+ if (issue.location.line !== undefined) {
293
+ location += `:${issue.location.line}`
294
+ if (issue.location.column !== undefined) {
295
+ location += `:${issue.location.column}`
296
+ }
297
+ }
298
+ lines.push(location)
299
+ lines.push('')
300
+ }
301
+
302
+ if (options.collapsibleDetails) {
303
+ lines.push('<details>')
304
+ lines.push('<summary>Details</summary>')
305
+ lines.push('')
306
+ }
307
+
308
+ lines.push(issue.description)
309
+ lines.push('')
310
+
311
+ if (options.includeSuggestions && issue.suggestion !== undefined) {
312
+ lines.push(`> **💡 Suggestion:** ${issue.suggestion}`)
313
+ lines.push('')
314
+ }
315
+
316
+ if (issue.relatedLocations !== undefined && issue.relatedLocations.length > 0) {
317
+ lines.push('**Related locations:**')
318
+ for (const loc of issue.relatedLocations) {
319
+ const relativePath = getRelativePath(loc.filePath, workspacePath)
320
+ let locStr = `- \`${relativePath}\``
321
+ if (loc.line !== undefined) {
322
+ locStr += `:${loc.line}`
323
+ }
324
+ lines.push(locStr)
325
+ }
326
+ lines.push('')
327
+ }
328
+
329
+ if (options.collapsibleDetails) {
330
+ lines.push('</details>')
331
+ }
332
+
333
+ return lines.join('\n')
334
+ }
335
+
336
+ /**
337
+ * Get status emoji based on severity.
338
+ */
339
+ function getStatusEmoji(severity: Severity | null, useEmoji: boolean): string {
340
+ if (!useEmoji) {
341
+ return ''
342
+ }
343
+
344
+ if (severity === null) {
345
+ return '✅'
346
+ }
347
+
348
+ return SEVERITY_CONFIG[severity].emoji
349
+ }