@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,381 @@
1
+ /**
2
+ * DuplicateDependencyAnalyzer - Detects duplicate and inconsistent dependencies across workspace packages.
3
+ *
4
+ * Identifies:
5
+ * - Same dependency with different versions across packages
6
+ * - Dependencies that could be hoisted to workspace root
7
+ * - Redundant devDependencies that match production dependencies
8
+ * - Version conflicts that may cause runtime issues
9
+ */
10
+
11
+ import type {WorkspacePackage} from '../scanner/workspace-scanner'
12
+ import type {Issue, IssueLocation, Severity} from '../types/index'
13
+ import type {Result} from '../types/result'
14
+ import type {AnalysisContext, Analyzer, AnalyzerError, AnalyzerMetadata} from './analyzer'
15
+
16
+ import {ok} from '@bfra.me/es/result'
17
+
18
+ import {createIssue, filterIssues} from './analyzer'
19
+
20
+ /**
21
+ * Configuration options specific to DuplicateDependencyAnalyzer.
22
+ */
23
+ export interface DuplicateDependencyAnalyzerOptions {
24
+ /** Ignore version differences for these packages */
25
+ readonly ignorePackages?: readonly string[]
26
+ /** Severity for version conflicts */
27
+ readonly conflictSeverity?: Severity
28
+ /** Whether to suggest hoisting common dependencies */
29
+ readonly suggestHoisting?: boolean
30
+ /** Minimum number of packages using a dependency to suggest hoisting */
31
+ readonly hoistingThreshold?: number
32
+ /** Check for redundant dev dependencies */
33
+ readonly checkRedundantDev?: boolean
34
+ }
35
+
36
+ const DEFAULT_OPTIONS: Required<DuplicateDependencyAnalyzerOptions> = {
37
+ ignorePackages: [],
38
+ conflictSeverity: 'warning',
39
+ suggestHoisting: true,
40
+ hoistingThreshold: 3,
41
+ checkRedundantDev: true,
42
+ }
43
+
44
+ export const duplicateDependencyAnalyzerMetadata: AnalyzerMetadata = {
45
+ id: 'duplicate-dependency',
46
+ name: 'Duplicate Dependency Analyzer',
47
+ description: 'Detects duplicate and version-inconsistent dependencies across workspace packages',
48
+ categories: ['dependency'],
49
+ defaultSeverity: 'warning',
50
+ }
51
+
52
+ /**
53
+ * Information about a dependency occurrence.
54
+ */
55
+ interface DependencyOccurrence {
56
+ readonly packageName: string
57
+ readonly version: string
58
+ readonly type: 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies'
59
+ readonly packageJsonPath: string
60
+ }
61
+
62
+ /**
63
+ * Aggregated dependency information across all packages.
64
+ */
65
+ interface DependencyInfo {
66
+ readonly dependencyName: string
67
+ readonly occurrences: readonly DependencyOccurrence[]
68
+ readonly versions: readonly string[]
69
+ readonly hasConflict: boolean
70
+ }
71
+
72
+ /**
73
+ * Creates a DuplicateDependencyAnalyzer instance.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const analyzer = createDuplicateDependencyAnalyzer({
78
+ * conflictSeverity: 'error',
79
+ * suggestHoisting: true,
80
+ * })
81
+ * const result = await analyzer.analyze(context)
82
+ * ```
83
+ */
84
+ export function createDuplicateDependencyAnalyzer(
85
+ options: DuplicateDependencyAnalyzerOptions = {},
86
+ ): Analyzer {
87
+ const resolvedOptions = {...DEFAULT_OPTIONS, ...options}
88
+
89
+ return {
90
+ metadata: duplicateDependencyAnalyzerMetadata,
91
+ analyze: async (context: AnalysisContext): Promise<Result<readonly Issue[], AnalyzerError>> => {
92
+ const issues: Issue[] = []
93
+
94
+ context.reportProgress?.('Collecting dependency information across workspace...')
95
+
96
+ const dependencyMap = collectDependencies(context.packages)
97
+
98
+ context.reportProgress?.('Analyzing for duplicates and conflicts...')
99
+
100
+ // Check for version conflicts
101
+ for (const [depName, info] of dependencyMap) {
102
+ if (isIgnored(depName, resolvedOptions.ignorePackages)) {
103
+ continue
104
+ }
105
+
106
+ if (info.hasConflict) {
107
+ issues.push(createVersionConflictIssue(info, resolvedOptions))
108
+ }
109
+
110
+ // Suggest hoisting if same version used in multiple packages
111
+ if (
112
+ resolvedOptions.suggestHoisting &&
113
+ info.occurrences.length >= resolvedOptions.hoistingThreshold &&
114
+ !info.hasConflict
115
+ ) {
116
+ issues.push(createHoistingSuggestion(info, resolvedOptions))
117
+ }
118
+ }
119
+
120
+ // Check for redundant dev dependencies
121
+ if (resolvedOptions.checkRedundantDev) {
122
+ for (const pkg of context.packages) {
123
+ issues.push(...checkRedundantDevDependencies(pkg))
124
+ }
125
+ }
126
+
127
+ return ok(filterIssues(issues, context.config))
128
+ },
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Collects all dependencies from all packages into a unified map.
134
+ */
135
+ function collectDependencies(packages: readonly WorkspacePackage[]): Map<string, DependencyInfo> {
136
+ const depMap = new Map<string, DependencyOccurrence[]>()
137
+
138
+ for (const pkg of packages) {
139
+ const pkgJson = pkg.packageJson
140
+
141
+ collectFromObject(depMap, pkg, pkgJson.dependencies, 'dependencies')
142
+ collectFromObject(depMap, pkg, pkgJson.devDependencies, 'devDependencies')
143
+ collectFromObject(depMap, pkg, pkgJson.peerDependencies, 'peerDependencies')
144
+ collectFromObject(depMap, pkg, pkgJson.optionalDependencies, 'optionalDependencies')
145
+ }
146
+
147
+ const result = new Map<string, DependencyInfo>()
148
+
149
+ for (const [depName, occurrences] of depMap) {
150
+ const versions = [...new Set(occurrences.map(o => normalizeVersion(o.version)))].sort()
151
+ const hasConflict = versions.length > 1
152
+
153
+ result.set(depName, {
154
+ dependencyName: depName,
155
+ occurrences,
156
+ versions,
157
+ hasConflict,
158
+ })
159
+ }
160
+
161
+ return result
162
+ }
163
+
164
+ /**
165
+ * Collects dependencies from a dependency object.
166
+ */
167
+ function collectFromObject(
168
+ depMap: Map<string, DependencyOccurrence[]>,
169
+ pkg: WorkspacePackage,
170
+ deps: Readonly<Record<string, string>> | undefined,
171
+ type: DependencyOccurrence['type'],
172
+ ): void {
173
+ if (deps === undefined) {
174
+ return
175
+ }
176
+
177
+ for (const [depName, version] of Object.entries(deps)) {
178
+ const occurrence: DependencyOccurrence = {
179
+ packageName: pkg.name,
180
+ version,
181
+ type,
182
+ packageJsonPath: pkg.packageJsonPath,
183
+ }
184
+
185
+ const existing = depMap.get(depName)
186
+ if (existing === undefined) {
187
+ depMap.set(depName, [occurrence])
188
+ } else {
189
+ existing.push(occurrence)
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Normalizes a version string for comparison.
196
+ * Removes workspace: prefix and leading ^ or ~ for basic comparison.
197
+ */
198
+ function normalizeVersion(version: string): string {
199
+ let normalized = version
200
+
201
+ // Handle workspace protocol
202
+ if (normalized.startsWith('workspace:')) {
203
+ normalized = normalized.slice('workspace:'.length)
204
+ }
205
+
206
+ // Keep the original if it's a range operator we want to compare
207
+ // Only normalize simple prefixes for exact matching
208
+ if (normalized.startsWith('^') || normalized.startsWith('~')) {
209
+ return normalized
210
+ }
211
+
212
+ return normalized
213
+ }
214
+
215
+ /**
216
+ * Checks if a dependency should be ignored.
217
+ */
218
+ function isIgnored(depName: string, ignorePackages: readonly string[]): boolean {
219
+ return ignorePackages.some(pattern => {
220
+ if (pattern.includes('*')) {
221
+ const regex = new RegExp(`^${pattern.replaceAll('*', '.*')}$`)
222
+ return regex.test(depName)
223
+ }
224
+ return depName === pattern
225
+ })
226
+ }
227
+
228
+ /**
229
+ * Creates an issue for a version conflict.
230
+ */
231
+ function createVersionConflictIssue(
232
+ info: DependencyInfo,
233
+ options: Required<DuplicateDependencyAnalyzerOptions>,
234
+ ): Issue {
235
+ const firstOccurrence = info.occurrences[0]
236
+ const location: IssueLocation = {
237
+ filePath: firstOccurrence?.packageJsonPath ?? '',
238
+ }
239
+
240
+ const relatedLocations: IssueLocation[] = info.occurrences.slice(1).map(o => ({
241
+ filePath: o.packageJsonPath,
242
+ }))
243
+
244
+ const versionList = info.occurrences
245
+ .map(o => ` - ${o.packageName}: ${o.version} (${o.type})`)
246
+ .join('\n')
247
+
248
+ return createIssue({
249
+ id: 'version-conflict',
250
+ title: `Version conflict: ${info.dependencyName}`,
251
+ description: `Dependency "${info.dependencyName}" has ${info.versions.length} different versions across packages:\n${versionList}`,
252
+ severity: options.conflictSeverity,
253
+ category: 'dependency',
254
+ location,
255
+ relatedLocations,
256
+ suggestion: `Align all packages to use the same version of "${info.dependencyName}" to avoid potential runtime issues`,
257
+ metadata: {
258
+ dependencyName: info.dependencyName,
259
+ versions: info.versions,
260
+ packages: info.occurrences.map(o => o.packageName),
261
+ },
262
+ })
263
+ }
264
+
265
+ /**
266
+ * Creates a suggestion to hoist a dependency.
267
+ */
268
+ function createHoistingSuggestion(
269
+ info: DependencyInfo,
270
+ _options: Required<DuplicateDependencyAnalyzerOptions>,
271
+ ): Issue {
272
+ const firstOccurrence = info.occurrences[0]
273
+ const location: IssueLocation = {
274
+ filePath: firstOccurrence?.packageJsonPath ?? '',
275
+ }
276
+
277
+ const packageCount = info.occurrences.length
278
+ const packageNames = info.occurrences.map(o => o.packageName).join(', ')
279
+
280
+ return createIssue({
281
+ id: 'hoisting-candidate',
282
+ title: `Hoisting candidate: ${info.dependencyName}`,
283
+ description: `Dependency "${info.dependencyName}" is used in ${packageCount} packages (${packageNames}) with the same version`,
284
+ severity: 'info',
285
+ category: 'dependency',
286
+ location,
287
+ suggestion: `Consider hoisting "${info.dependencyName}" to the workspace root package.json to reduce duplication`,
288
+ metadata: {
289
+ dependencyName: info.dependencyName,
290
+ version: info.versions[0],
291
+ packageCount,
292
+ packages: info.occurrences.map(o => o.packageName),
293
+ },
294
+ })
295
+ }
296
+
297
+ /**
298
+ * Checks for redundant dev dependencies (same package in both deps and devDeps).
299
+ */
300
+ function checkRedundantDevDependencies(pkg: WorkspacePackage): Issue[] {
301
+ const issues: Issue[] = []
302
+ const pkgJson = pkg.packageJson
303
+
304
+ if (pkgJson.dependencies === undefined || pkgJson.devDependencies === undefined) {
305
+ return issues
306
+ }
307
+
308
+ for (const [depName, devVersion] of Object.entries(pkgJson.devDependencies)) {
309
+ if (depName in pkgJson.dependencies) {
310
+ const prodVersion = pkgJson.dependencies[depName]
311
+
312
+ issues.push(
313
+ createIssue({
314
+ id: 'redundant-dev-dependency',
315
+ title: `Redundant dev dependency: ${depName}`,
316
+ description: `Package "${pkg.name}" has "${depName}" in both dependencies (${prodVersion}) and devDependencies (${devVersion})`,
317
+ severity: 'info',
318
+ category: 'dependency',
319
+ location: {filePath: pkg.packageJsonPath},
320
+ suggestion: `Remove "${depName}" from devDependencies as it's already in dependencies`,
321
+ metadata: {
322
+ packageName: pkg.name,
323
+ dependencyName: depName,
324
+ prodVersion,
325
+ devVersion,
326
+ },
327
+ }),
328
+ )
329
+ }
330
+ }
331
+
332
+ return issues
333
+ }
334
+
335
+ /**
336
+ * Aggregates duplicate dependency statistics.
337
+ */
338
+ export interface DuplicateDependencyStats {
339
+ /** Total unique dependencies across workspace */
340
+ readonly totalUniqueDependencies: number
341
+ /** Number of dependencies with version conflicts */
342
+ readonly conflictingDependencies: number
343
+ /** Number of packages with redundant dev dependencies */
344
+ readonly packagesWithRedundantDev: number
345
+ /** Dependencies appearing in most packages */
346
+ readonly mostCommon: readonly {readonly name: string; readonly count: number}[]
347
+ /** Dependencies with the most version variants */
348
+ readonly mostVariants: readonly {readonly name: string; readonly versionCount: number}[]
349
+ }
350
+
351
+ /**
352
+ * Computes statistics about dependency duplicates.
353
+ */
354
+ export function computeDuplicateStats(
355
+ dependencyMap: ReadonlyMap<string, DependencyInfo>,
356
+ topN = 10,
357
+ ): DuplicateDependencyStats {
358
+ let conflictingDependencies = 0
359
+ const commonDeps: {name: string; count: number}[] = []
360
+ const variantDeps: {name: string; versionCount: number}[] = []
361
+
362
+ for (const [name, info] of dependencyMap) {
363
+ if (info.hasConflict) {
364
+ conflictingDependencies++
365
+ }
366
+
367
+ commonDeps.push({name, count: info.occurrences.length})
368
+ variantDeps.push({name, versionCount: info.versions.length})
369
+ }
370
+
371
+ commonDeps.sort((a, b) => b.count - a.count)
372
+ variantDeps.sort((a, b) => b.versionCount - a.versionCount)
373
+
374
+ return {
375
+ totalUniqueDependencies: dependencyMap.size,
376
+ conflictingDependencies,
377
+ packagesWithRedundantDev: 0, // Computed separately per-package
378
+ mostCommon: commonDeps.slice(0, topN),
379
+ mostVariants: variantDeps.filter(d => d.versionCount > 1).slice(0, topN),
380
+ }
381
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * EslintConfigAnalyzer - Validates ESLint configuration for consistency and best practices.
3
+ *
4
+ * Detects issues such as:
5
+ * - Missing ESLint configuration files
6
+ * - Inconsistent config patterns across packages
7
+ * - Missing recommended plugins for TypeScript packages
8
+ */
9
+
10
+ import type {WorkspacePackage} from '../scanner/workspace-scanner'
11
+ import type {Issue, IssueLocation} from '../types/index'
12
+ import type {Result} from '../types/result'
13
+ import type {AnalysisContext, Analyzer, AnalyzerError, AnalyzerMetadata} from './analyzer'
14
+
15
+ import fs from 'node:fs/promises'
16
+ import path from 'node:path'
17
+
18
+ import {ok} from '@bfra.me/es/result'
19
+
20
+ import {createIssue, filterIssues} from './analyzer'
21
+
22
+ /**
23
+ * Configuration options specific to EslintConfigAnalyzer.
24
+ */
25
+ export interface EslintConfigAnalyzerOptions {
26
+ /** Whether to require ESLint config for all packages */
27
+ readonly requireConfig?: boolean
28
+ /** Expected config file pattern (e.g., 'eslint.config.ts') */
29
+ readonly expectedConfigFile?: string
30
+ /** Whether to check for flat config usage */
31
+ readonly requireFlatConfig?: boolean
32
+ /** Package names exempt from checks */
33
+ readonly exemptPackages?: readonly string[]
34
+ }
35
+
36
+ const DEFAULT_OPTIONS: EslintConfigAnalyzerOptions = {
37
+ requireConfig: true,
38
+ expectedConfigFile: 'eslint.config.ts',
39
+ requireFlatConfig: true,
40
+ exemptPackages: [],
41
+ }
42
+
43
+ const METADATA: AnalyzerMetadata = {
44
+ id: 'eslint-config',
45
+ name: 'ESLint Config Analyzer',
46
+ description: 'Validates ESLint configuration for consistency and best practices',
47
+ categories: ['configuration'],
48
+ defaultSeverity: 'warning',
49
+ }
50
+
51
+ const ESLINT_CONFIG_FILES = [
52
+ 'eslint.config.ts',
53
+ 'eslint.config.mts',
54
+ 'eslint.config.cts',
55
+ 'eslint.config.js',
56
+ 'eslint.config.mjs',
57
+ 'eslint.config.cjs',
58
+ '.eslintrc.json',
59
+ '.eslintrc.js',
60
+ '.eslintrc.cjs',
61
+ '.eslintrc.yaml',
62
+ '.eslintrc.yml',
63
+ '.eslintrc',
64
+ ] as const
65
+
66
+ /**
67
+ * Creates an EslintConfigAnalyzer instance.
68
+ */
69
+ export function createEslintConfigAnalyzer(options: EslintConfigAnalyzerOptions = {}): Analyzer {
70
+ const resolvedOptions = {...DEFAULT_OPTIONS, ...options}
71
+
72
+ return {
73
+ metadata: METADATA,
74
+ analyze: async (context: AnalysisContext): Promise<Result<readonly Issue[], AnalyzerError>> => {
75
+ const issues: Issue[] = []
76
+
77
+ for (const pkg of context.packages) {
78
+ if (isExemptPackage(pkg.name, resolvedOptions.exemptPackages)) {
79
+ continue
80
+ }
81
+
82
+ const packageIssues = await analyzePackageEslint(pkg, resolvedOptions)
83
+ issues.push(...packageIssues)
84
+ }
85
+
86
+ return ok(filterIssues(issues, context.config))
87
+ },
88
+ }
89
+ }
90
+
91
+ function isExemptPackage(name: string, exemptPackages: readonly string[] | undefined): boolean {
92
+ return exemptPackages?.includes(name) ?? false
93
+ }
94
+
95
+ function createLocation(filePath: string): IssueLocation {
96
+ return {filePath}
97
+ }
98
+
99
+ async function analyzePackageEslint(
100
+ pkg: WorkspacePackage,
101
+ options: EslintConfigAnalyzerOptions,
102
+ ): Promise<Issue[]> {
103
+ const issues: Issue[] = []
104
+
105
+ // Find existing ESLint config
106
+ const configResult = await findEslintConfig(pkg.packagePath)
107
+
108
+ if (!configResult.found) {
109
+ if (options.requireConfig) {
110
+ issues.push(
111
+ createIssue({
112
+ id: 'eslint-no-config',
113
+ title: 'Missing ESLint configuration',
114
+ description: `Package "${pkg.name}" has no ESLint configuration file`,
115
+ severity: 'warning',
116
+ category: 'configuration',
117
+ location: createLocation(path.join(pkg.packagePath, 'package.json')),
118
+ suggestion: `Create "${options.expectedConfigFile ?? 'eslint.config.ts'}" for code quality enforcement`,
119
+ }),
120
+ )
121
+ }
122
+ return issues
123
+ }
124
+
125
+ // Check for legacy config files
126
+ if (options.requireFlatConfig && configResult.isLegacy) {
127
+ issues.push(
128
+ createIssue({
129
+ id: 'eslint-legacy-config',
130
+ title: 'Using legacy ESLint configuration',
131
+ description: `Package "${pkg.name}" uses legacy config format "${configResult.fileName}"`,
132
+ severity: 'info',
133
+ category: 'configuration',
134
+ location: createLocation(configResult.filePath),
135
+ suggestion: 'Migrate to flat config format (eslint.config.ts) for ESLint v9+',
136
+ }),
137
+ )
138
+ }
139
+
140
+ // Check if expected config file is used
141
+ if (
142
+ options.expectedConfigFile !== undefined &&
143
+ configResult.fileName !== options.expectedConfigFile &&
144
+ !configResult.isLegacy
145
+ ) {
146
+ issues.push(
147
+ createIssue({
148
+ id: 'eslint-unexpected-config-file',
149
+ title: 'Unexpected ESLint config file name',
150
+ description: `Package "${pkg.name}" uses "${configResult.fileName}" instead of "${options.expectedConfigFile}"`,
151
+ severity: 'info',
152
+ category: 'configuration',
153
+ location: createLocation(configResult.filePath),
154
+ suggestion: `Consider renaming to "${options.expectedConfigFile}" for consistency`,
155
+ }),
156
+ )
157
+ }
158
+
159
+ // Analyze config content for common issues
160
+ if (!configResult.isLegacy) {
161
+ const contentIssues = await analyzeEslintConfigContent(pkg, configResult.filePath)
162
+ issues.push(...contentIssues)
163
+ }
164
+
165
+ return issues
166
+ }
167
+
168
+ interface EslintConfigResult {
169
+ found: boolean
170
+ fileName: string
171
+ filePath: string
172
+ isLegacy: boolean
173
+ }
174
+
175
+ async function findEslintConfig(packagePath: string): Promise<EslintConfigResult> {
176
+ for (const configFile of ESLINT_CONFIG_FILES) {
177
+ const configPath = path.join(packagePath, configFile)
178
+ if (await fileExists(configPath)) {
179
+ return {
180
+ found: true,
181
+ fileName: configFile,
182
+ filePath: configPath,
183
+ isLegacy: isLegacyConfig(configFile),
184
+ }
185
+ }
186
+ }
187
+
188
+ return {
189
+ found: false,
190
+ fileName: '',
191
+ filePath: '',
192
+ isLegacy: false,
193
+ }
194
+ }
195
+
196
+ function isLegacyConfig(fileName: string): boolean {
197
+ return fileName.startsWith('.eslintrc')
198
+ }
199
+
200
+ async function fileExists(filePath: string): Promise<boolean> {
201
+ try {
202
+ await fs.access(filePath)
203
+ return true
204
+ } catch {
205
+ return false
206
+ }
207
+ }
208
+
209
+ async function analyzeEslintConfigContent(
210
+ pkg: WorkspacePackage,
211
+ configPath: string,
212
+ ): Promise<Issue[]> {
213
+ const issues: Issue[] = []
214
+
215
+ let content: string
216
+ try {
217
+ content = await fs.readFile(configPath, 'utf-8')
218
+ } catch {
219
+ return issues
220
+ }
221
+
222
+ // Check for defineConfig usage
223
+ if (!content.includes('defineConfig')) {
224
+ issues.push(
225
+ createIssue({
226
+ id: 'eslint-no-define-config',
227
+ title: 'ESLint config not using defineConfig',
228
+ description: `Package "${pkg.name}" ESLint config may not be using defineConfig() helper`,
229
+ severity: 'info',
230
+ category: 'configuration',
231
+ location: createLocation(configPath),
232
+ suggestion: 'Use defineConfig() from your ESLint config package for better type safety',
233
+ }),
234
+ )
235
+ }
236
+
237
+ // Check for TypeScript plugin in TS packages
238
+ if (pkg.hasTsConfig) {
239
+ const hasTypescriptPlugin =
240
+ content.includes('typescript-eslint') ||
241
+ content.includes('@typescript-eslint') ||
242
+ content.includes('tseslint')
243
+
244
+ if (!hasTypescriptPlugin) {
245
+ issues.push(
246
+ createIssue({
247
+ id: 'eslint-no-typescript-plugin',
248
+ title: 'ESLint config missing TypeScript support',
249
+ description: `Package "${pkg.name}" is a TypeScript package but ESLint config may not include TypeScript plugin`,
250
+ severity: 'warning',
251
+ category: 'configuration',
252
+ location: createLocation(configPath),
253
+ suggestion: 'Add @typescript-eslint/eslint-plugin for TypeScript-specific linting rules',
254
+ }),
255
+ )
256
+ }
257
+ }
258
+
259
+ // Check for Prettier integration
260
+ const hasPrettierConfig =
261
+ content.includes('prettier') || content.includes('eslint-config-prettier')
262
+
263
+ if (!hasPrettierConfig) {
264
+ issues.push(
265
+ createIssue({
266
+ id: 'eslint-no-prettier',
267
+ title: 'ESLint config may not include Prettier integration',
268
+ description: `Package "${pkg.name}" ESLint config may not disable Prettier-conflicting rules`,
269
+ severity: 'info',
270
+ category: 'configuration',
271
+ location: createLocation(configPath),
272
+ suggestion:
273
+ 'Consider adding eslint-config-prettier to avoid formatting conflicts with Prettier',
274
+ }),
275
+ )
276
+ }
277
+
278
+ return issues
279
+ }
280
+
281
+ export {METADATA as eslintConfigAnalyzerMetadata}