@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,335 @@
1
+ /**
2
+ * ConfigConsistencyAnalyzer - Cross-validates configuration across multiple config files.
3
+ *
4
+ * Detects issues such as:
5
+ * - Mismatches between package.json exports and tsconfig outDir
6
+ * - TypeScript target/module inconsistencies with package.json type
7
+ * - Build output directory mismatches
8
+ * - Inconsistent configurations across workspace packages
9
+ */
10
+
11
+ import type {ParsedPackageJson, ParsedTsConfig} from '../parser/config-parser'
12
+ import type {WorkspacePackage} from '../scanner/workspace-scanner'
13
+ import type {Issue, IssueLocation} from '../types/index'
14
+ import type {Result} from '../types/result'
15
+ import type {AnalysisContext, Analyzer, AnalyzerError, AnalyzerMetadata} from './analyzer'
16
+
17
+ import path from 'node:path'
18
+
19
+ import {ok} from '@bfra.me/es/result'
20
+
21
+ import {parseTsConfig} from '../parser/config-parser'
22
+ import {createIssue, filterIssues} from './analyzer'
23
+
24
+ /**
25
+ * Configuration options specific to ConfigConsistencyAnalyzer.
26
+ */
27
+ export interface ConfigConsistencyAnalyzerOptions {
28
+ /** Whether to check tsconfig/package.json consistency */
29
+ readonly checkTsconfigPackageJson?: boolean
30
+ /** Whether to check cross-package consistency */
31
+ readonly checkCrossPackage?: boolean
32
+ /** Package names exempt from checks */
33
+ readonly exemptPackages?: readonly string[]
34
+ }
35
+
36
+ const DEFAULT_OPTIONS: ConfigConsistencyAnalyzerOptions = {
37
+ checkTsconfigPackageJson: true,
38
+ checkCrossPackage: true,
39
+ exemptPackages: [],
40
+ }
41
+
42
+ const METADATA: AnalyzerMetadata = {
43
+ id: 'config-consistency',
44
+ name: 'Configuration Consistency Analyzer',
45
+ description:
46
+ 'Cross-validates configuration across package.json, tsconfig.json, and build configs',
47
+ categories: ['configuration'],
48
+ defaultSeverity: 'warning',
49
+ }
50
+
51
+ /**
52
+ * Creates a ConfigConsistencyAnalyzer instance.
53
+ */
54
+ export function createConfigConsistencyAnalyzer(
55
+ options: ConfigConsistencyAnalyzerOptions = {},
56
+ ): Analyzer {
57
+ const resolvedOptions = {...DEFAULT_OPTIONS, ...options}
58
+
59
+ return {
60
+ metadata: METADATA,
61
+ analyze: async (context: AnalysisContext): Promise<Result<readonly Issue[], AnalyzerError>> => {
62
+ const issues: Issue[] = []
63
+
64
+ // Analyze individual package consistency
65
+ for (const pkg of context.packages) {
66
+ if (isExemptPackage(pkg.name, resolvedOptions.exemptPackages)) {
67
+ continue
68
+ }
69
+
70
+ if (resolvedOptions.checkTsconfigPackageJson) {
71
+ const packageIssues = await analyzePackageConsistency(pkg)
72
+ issues.push(...packageIssues)
73
+ }
74
+ }
75
+
76
+ // Analyze cross-package consistency
77
+ if (resolvedOptions.checkCrossPackage) {
78
+ const crossPackageIssues = analyzeCrossPackageConsistency(context.packages)
79
+ issues.push(...crossPackageIssues)
80
+ }
81
+
82
+ return ok(filterIssues(issues, context.config))
83
+ },
84
+ }
85
+ }
86
+
87
+ function isExemptPackage(name: string, exemptPackages: readonly string[] | undefined): boolean {
88
+ return exemptPackages?.includes(name) ?? false
89
+ }
90
+
91
+ function createLocation(filePath: string): IssueLocation {
92
+ return {filePath}
93
+ }
94
+
95
+ async function analyzePackageConsistency(pkg: WorkspacePackage): Promise<Issue[]> {
96
+ const issues: Issue[] = []
97
+ const pkgJson = pkg.packageJson as ParsedPackageJson
98
+
99
+ // Only analyze TypeScript packages
100
+ if (!pkg.hasTsConfig) {
101
+ return issues
102
+ }
103
+
104
+ const tsconfigPath = path.join(pkg.packagePath, 'tsconfig.json')
105
+ const tsconfigResult = await parseTsConfig(tsconfigPath)
106
+
107
+ if (!tsconfigResult.success) {
108
+ return issues
109
+ }
110
+
111
+ const tsconfig = tsconfigResult.data
112
+
113
+ // Check package.json type vs tsconfig module settings
114
+ issues.push(...checkTypeModuleConsistency(pkg, pkgJson, tsconfig))
115
+
116
+ // Check outDir vs package.json entry points
117
+ issues.push(...checkOutDirConsistency(pkg, pkgJson, tsconfig))
118
+
119
+ // Check rootDir configuration
120
+ issues.push(...checkRootDirConsistency(pkg, tsconfig))
121
+
122
+ return issues
123
+ }
124
+
125
+ function checkTypeModuleConsistency(
126
+ pkg: WorkspacePackage,
127
+ pkgJson: ParsedPackageJson,
128
+ tsconfig: ParsedTsConfig,
129
+ ): Issue[] {
130
+ const issues: Issue[] = []
131
+ const opts = tsconfig.compilerOptions
132
+
133
+ if (opts?.module === undefined) {
134
+ return issues
135
+ }
136
+
137
+ const moduleValue = opts.module.toLowerCase()
138
+ const isEsmPackage = pkgJson.type === 'module'
139
+
140
+ // ESM package should use ESM-compatible module setting
141
+ const esmModules = ['esnext', 'es2020', 'es2022', 'node16', 'nodenext']
142
+ const isEsmModule = esmModules.some(m => moduleValue.includes(m))
143
+
144
+ if (isEsmPackage && !isEsmModule) {
145
+ issues.push(
146
+ createIssue({
147
+ id: 'config-esm-cjs-module',
148
+ title: 'ESM package with non-ESM tsconfig module',
149
+ description: `Package "${pkg.name}" has "type": "module" but tsconfig uses "${opts.module}"`,
150
+ severity: 'warning',
151
+ category: 'configuration',
152
+ location: createLocation(tsconfig.filePath),
153
+ suggestion: 'Consider using "module": "NodeNext" or "ESNext" for ESM packages',
154
+ metadata: {packageType: pkgJson.type, tsconfigModule: opts.module},
155
+ }),
156
+ )
157
+ }
158
+
159
+ // Non-ESM package using ESM module might be intentional but worth noting
160
+ if (!isEsmPackage && isEsmModule) {
161
+ issues.push(
162
+ createIssue({
163
+ id: 'config-cjs-esm-module',
164
+ title: 'CJS package with ESM tsconfig module',
165
+ description: `Package "${pkg.name}" is CJS but tsconfig uses "${opts.module}"`,
166
+ severity: 'info',
167
+ category: 'configuration',
168
+ location: createLocation(tsconfig.filePath),
169
+ suggestion:
170
+ 'Ensure build tooling transpiles to CJS format, or add "type": "module" to package.json',
171
+ metadata: {packageType: pkgJson.type ?? 'commonjs', tsconfigModule: opts.module},
172
+ }),
173
+ )
174
+ }
175
+
176
+ return issues
177
+ }
178
+
179
+ function checkOutDirConsistency(
180
+ pkg: WorkspacePackage,
181
+ pkgJson: ParsedPackageJson,
182
+ tsconfig: ParsedTsConfig,
183
+ ): Issue[] {
184
+ const issues: Issue[] = []
185
+ const opts = tsconfig.compilerOptions
186
+
187
+ if (opts?.outDir === undefined) {
188
+ return issues
189
+ }
190
+
191
+ const outDir = normalizePathSegment(opts.outDir)
192
+
193
+ // Check if main entry point uses outDir
194
+ if (pkgJson.main !== undefined) {
195
+ const mainDir = normalizePathSegment(pkgJson.main.split('/')[0] ?? '')
196
+
197
+ if (outDir !== mainDir && mainDir !== '' && !mainDir.startsWith('.')) {
198
+ issues.push(
199
+ createIssue({
200
+ id: 'config-outdir-main-mismatch',
201
+ title: 'tsconfig outDir does not match main entry',
202
+ description: `Package "${pkg.name}" tsconfig outDir "${opts.outDir}" but main points to "${mainDir}/"`,
203
+ severity: 'warning',
204
+ category: 'configuration',
205
+ location: createLocation(tsconfig.filePath),
206
+ suggestion: `Align tsconfig outDir with package.json main entry directory`,
207
+ metadata: {outDir: opts.outDir, main: pkgJson.main},
208
+ }),
209
+ )
210
+ }
211
+ }
212
+
213
+ // Check exports field consistency
214
+ if (pkgJson.exports !== undefined) {
215
+ const exportPaths = extractExportPaths(pkgJson.exports)
216
+ const mismatchedExports = exportPaths.filter(p => {
217
+ const exportDir = normalizePathSegment(p.split('/')[0] ?? '')
218
+ return exportDir !== '' && exportDir !== outDir && !exportDir.startsWith('.')
219
+ })
220
+
221
+ if (mismatchedExports.length > 0) {
222
+ issues.push(
223
+ createIssue({
224
+ id: 'config-outdir-exports-mismatch',
225
+ title: 'tsconfig outDir does not match some exports',
226
+ description: `Package "${pkg.name}" has exports that don't match tsconfig outDir "${opts.outDir}"`,
227
+ severity: 'info',
228
+ category: 'configuration',
229
+ location: createLocation(tsconfig.filePath),
230
+ suggestion: 'Verify exports paths are correct and match build output',
231
+ metadata: {outDir: opts.outDir, mismatchedExports},
232
+ }),
233
+ )
234
+ }
235
+ }
236
+
237
+ return issues
238
+ }
239
+
240
+ function checkRootDirConsistency(pkg: WorkspacePackage, tsconfig: ParsedTsConfig): Issue[] {
241
+ const issues: Issue[] = []
242
+ const opts = tsconfig.compilerOptions
243
+
244
+ // Check if rootDir is set for packages with src directory
245
+ if (opts?.rootDir === undefined && pkg.srcPath !== '') {
246
+ // Check if src exists
247
+ const srcDirExists = pkg.sourceFiles.some(f => f.includes('/src/'))
248
+
249
+ if (srcDirExists) {
250
+ issues.push(
251
+ createIssue({
252
+ id: 'config-no-rootdir',
253
+ title: 'tsconfig missing rootDir',
254
+ description: `Package "${pkg.name}" has src/ directory but tsconfig does not set rootDir`,
255
+ severity: 'info',
256
+ category: 'configuration',
257
+ location: createLocation(tsconfig.filePath),
258
+ suggestion: 'Add "rootDir": "./src" for cleaner output structure',
259
+ }),
260
+ )
261
+ }
262
+ }
263
+
264
+ return issues
265
+ }
266
+
267
+ function analyzeCrossPackageConsistency(packages: readonly WorkspacePackage[]): Issue[] {
268
+ const issues: Issue[] = []
269
+
270
+ // Check for consistent package.json type across workspace
271
+ const typeModule = packages.filter(p => (p.packageJson as ParsedPackageJson).type === 'module')
272
+ const typeCjs = packages.filter(p => (p.packageJson as ParsedPackageJson).type !== 'module')
273
+
274
+ // Mixed types might be intentional, but worth noting if mostly one type
275
+ const total = packages.length
276
+ if (total > 3) {
277
+ const moduleRatio = typeModule.length / total
278
+ const cjsRatio = typeCjs.length / total
279
+
280
+ if (moduleRatio > 0.7 && typeCjs.length > 0) {
281
+ const cjsNames = typeCjs.map(p => p.name).slice(0, 5)
282
+ issues.push(
283
+ createIssue({
284
+ id: 'config-mixed-module-types',
285
+ title: 'Mixed module types in workspace',
286
+ description: `Workspace is mostly ESM but ${typeCjs.length} package(s) use CJS: ${cjsNames.join(', ')}${typeCjs.length > 5 ? '...' : ''}`,
287
+ severity: 'info',
288
+ category: 'configuration',
289
+ location: createLocation(packages[0]?.packageJsonPath ?? ''),
290
+ suggestion: 'Consider migrating remaining packages to ESM for consistency',
291
+ metadata: {esmCount: typeModule.length, cjsCount: typeCjs.length, cjsNames},
292
+ }),
293
+ )
294
+ } else if (cjsRatio > 0.7 && typeModule.length > 0) {
295
+ const esmNames = typeModule.map(p => p.name).slice(0, 5)
296
+ issues.push(
297
+ createIssue({
298
+ id: 'config-mixed-module-types',
299
+ title: 'Mixed module types in workspace',
300
+ description: `Workspace is mostly CJS but ${typeModule.length} package(s) use ESM: ${esmNames.join(', ')}${typeModule.length > 5 ? '...' : ''}`,
301
+ severity: 'info',
302
+ category: 'configuration',
303
+ location: createLocation(packages[0]?.packageJsonPath ?? ''),
304
+ suggestion: 'Consider standardizing module format across workspace',
305
+ metadata: {esmCount: typeModule.length, cjsCount: typeCjs.length, esmNames},
306
+ }),
307
+ )
308
+ }
309
+ }
310
+
311
+ return issues
312
+ }
313
+
314
+ function normalizePathSegment(segment: string): string {
315
+ return segment.replace(/^\.\//, '').replace(/\/$/, '')
316
+ }
317
+
318
+ function extractExportPaths(exports: Record<string, unknown>): string[] {
319
+ const paths: string[] = []
320
+
321
+ function walk(value: unknown): void {
322
+ if (typeof value === 'string') {
323
+ paths.push(value)
324
+ } else if (typeof value === 'object' && value !== null) {
325
+ for (const v of Object.values(value)) {
326
+ walk(v)
327
+ }
328
+ }
329
+ }
330
+
331
+ walk(exports)
332
+ return paths
333
+ }
334
+
335
+ export {METADATA as configConsistencyAnalyzerMetadata}