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