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