@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,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TsconfigAnalyzer - Validates tsconfig.json configuration for consistency and best practices.
|
|
3
|
+
*
|
|
4
|
+
* Detects issues such as:
|
|
5
|
+
* - Missing recommended compiler options
|
|
6
|
+
* - Inconsistent settings across packages
|
|
7
|
+
* - Invalid path mappings
|
|
8
|
+
* - Missing project references
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {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 TsconfigAnalyzer.
|
|
26
|
+
*/
|
|
27
|
+
export interface TsconfigAnalyzerOptions {
|
|
28
|
+
/** Whether to check for strict mode */
|
|
29
|
+
readonly requireStrict?: boolean
|
|
30
|
+
/** Whether to require explicit module settings */
|
|
31
|
+
readonly requireModuleSettings?: boolean
|
|
32
|
+
/** Whether to check path mapping consistency */
|
|
33
|
+
readonly checkPaths?: boolean
|
|
34
|
+
/** Whether to require project references in monorepo */
|
|
35
|
+
readonly requireReferences?: boolean
|
|
36
|
+
/** Package names exempt from certain checks */
|
|
37
|
+
readonly exemptPackages?: readonly string[]
|
|
38
|
+
/** Expected base tsconfig to extend from */
|
|
39
|
+
readonly expectedExtends?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_OPTIONS: TsconfigAnalyzerOptions = {
|
|
43
|
+
requireStrict: true,
|
|
44
|
+
requireModuleSettings: true,
|
|
45
|
+
checkPaths: true,
|
|
46
|
+
requireReferences: false,
|
|
47
|
+
exemptPackages: [],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const METADATA: AnalyzerMetadata = {
|
|
51
|
+
id: 'tsconfig',
|
|
52
|
+
name: 'TSConfig Analyzer',
|
|
53
|
+
description: 'Validates tsconfig.json configuration for consistency and best practices',
|
|
54
|
+
categories: ['configuration'],
|
|
55
|
+
defaultSeverity: 'warning',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a TsconfigAnalyzer instance.
|
|
60
|
+
*/
|
|
61
|
+
export function createTsconfigAnalyzer(options: TsconfigAnalyzerOptions = {}): Analyzer {
|
|
62
|
+
const resolvedOptions = {...DEFAULT_OPTIONS, ...options}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
metadata: METADATA,
|
|
66
|
+
analyze: async (context: AnalysisContext): Promise<Result<readonly Issue[], AnalyzerError>> => {
|
|
67
|
+
const issues: Issue[] = []
|
|
68
|
+
|
|
69
|
+
for (const pkg of context.packages) {
|
|
70
|
+
if (!pkg.hasTsConfig) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isExemptPackage(pkg.name, resolvedOptions.exemptPackages)) {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const tsconfigPath = path.join(pkg.packagePath, 'tsconfig.json')
|
|
79
|
+
const parseResult = await parseTsConfig(tsconfigPath)
|
|
80
|
+
|
|
81
|
+
if (!parseResult.success) {
|
|
82
|
+
issues.push(
|
|
83
|
+
createIssue({
|
|
84
|
+
id: 'tsconfig-parse-error',
|
|
85
|
+
title: 'Failed to parse tsconfig.json',
|
|
86
|
+
description: `Package "${pkg.name}": ${parseResult.error.message}`,
|
|
87
|
+
severity: 'error',
|
|
88
|
+
category: 'configuration',
|
|
89
|
+
location: {filePath: tsconfigPath},
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const packageIssues = analyzePackageTsconfig(
|
|
96
|
+
pkg,
|
|
97
|
+
parseResult.data,
|
|
98
|
+
resolvedOptions,
|
|
99
|
+
context.packages,
|
|
100
|
+
)
|
|
101
|
+
issues.push(...packageIssues)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return ok(filterIssues(issues, context.config))
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isExemptPackage(name: string, exemptPackages: readonly string[] | undefined): boolean {
|
|
110
|
+
return exemptPackages?.includes(name) ?? false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function createLocation(filePath: string): IssueLocation {
|
|
114
|
+
return {filePath}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function analyzePackageTsconfig(
|
|
118
|
+
pkg: WorkspacePackage,
|
|
119
|
+
tsconfig: ParsedTsConfig,
|
|
120
|
+
options: TsconfigAnalyzerOptions,
|
|
121
|
+
allPackages: readonly WorkspacePackage[],
|
|
122
|
+
): Issue[] {
|
|
123
|
+
const issues: Issue[] = []
|
|
124
|
+
|
|
125
|
+
// Check extends
|
|
126
|
+
if (options.expectedExtends !== undefined) {
|
|
127
|
+
issues.push(...checkExtends(pkg, tsconfig, options.expectedExtends))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check strict mode
|
|
131
|
+
if (options.requireStrict) {
|
|
132
|
+
issues.push(...checkStrictMode(pkg, tsconfig))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check module settings
|
|
136
|
+
if (options.requireModuleSettings) {
|
|
137
|
+
issues.push(...checkModuleSettings(pkg, tsconfig))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check path mappings
|
|
141
|
+
if (options.checkPaths) {
|
|
142
|
+
issues.push(...checkPathMappings(pkg, tsconfig, allPackages))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check project references
|
|
146
|
+
if (options.requireReferences) {
|
|
147
|
+
issues.push(...checkReferences(pkg, tsconfig, allPackages))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check common issues
|
|
151
|
+
issues.push(...checkCommonIssues(pkg, tsconfig))
|
|
152
|
+
|
|
153
|
+
return issues
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function checkExtends(
|
|
157
|
+
pkg: WorkspacePackage,
|
|
158
|
+
tsconfig: ParsedTsConfig,
|
|
159
|
+
expectedExtends: string,
|
|
160
|
+
): Issue[] {
|
|
161
|
+
const issues: Issue[] = []
|
|
162
|
+
|
|
163
|
+
if (tsconfig.extends === undefined) {
|
|
164
|
+
issues.push(
|
|
165
|
+
createIssue({
|
|
166
|
+
id: 'tsconfig-no-extends',
|
|
167
|
+
title: 'TSConfig does not extend shared config',
|
|
168
|
+
description: `Package "${pkg.name}" does not extend "${expectedExtends}"`,
|
|
169
|
+
severity: 'warning',
|
|
170
|
+
category: 'configuration',
|
|
171
|
+
location: createLocation(tsconfig.filePath),
|
|
172
|
+
suggestion: `Add "extends": "${expectedExtends}" for consistent configuration`,
|
|
173
|
+
}),
|
|
174
|
+
)
|
|
175
|
+
return issues
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const extendsArray: readonly string[] = Array.isArray(tsconfig.extends)
|
|
179
|
+
? tsconfig.extends
|
|
180
|
+
: [tsconfig.extends]
|
|
181
|
+
const firstExtends = extendsArray[0]
|
|
182
|
+
|
|
183
|
+
if (typeof firstExtends === 'string' && !firstExtends.includes(expectedExtends)) {
|
|
184
|
+
issues.push(
|
|
185
|
+
createIssue({
|
|
186
|
+
id: 'tsconfig-wrong-extends',
|
|
187
|
+
title: 'TSConfig extends unexpected config',
|
|
188
|
+
description: `Package "${pkg.name}" extends "${firstExtends}" instead of "${expectedExtends}"`,
|
|
189
|
+
severity: 'info',
|
|
190
|
+
category: 'configuration',
|
|
191
|
+
location: createLocation(tsconfig.filePath),
|
|
192
|
+
suggestion: `Consider extending "${expectedExtends}" for consistent configuration`,
|
|
193
|
+
}),
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return issues
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function checkStrictMode(pkg: WorkspacePackage, tsconfig: ParsedTsConfig): Issue[] {
|
|
201
|
+
const issues: Issue[] = []
|
|
202
|
+
const opts = tsconfig.compilerOptions
|
|
203
|
+
|
|
204
|
+
if (opts?.strict !== true) {
|
|
205
|
+
issues.push(
|
|
206
|
+
createIssue({
|
|
207
|
+
id: 'tsconfig-not-strict',
|
|
208
|
+
title: 'TSConfig strict mode disabled',
|
|
209
|
+
description: `Package "${pkg.name}" does not have "strict": true in compilerOptions`,
|
|
210
|
+
severity: 'warning',
|
|
211
|
+
category: 'configuration',
|
|
212
|
+
location: createLocation(tsconfig.filePath),
|
|
213
|
+
suggestion: 'Enable "strict": true for better type safety',
|
|
214
|
+
}),
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return issues
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function checkModuleSettings(pkg: WorkspacePackage, tsconfig: ParsedTsConfig): Issue[] {
|
|
222
|
+
const issues: Issue[] = []
|
|
223
|
+
const opts = tsconfig.compilerOptions
|
|
224
|
+
|
|
225
|
+
// Check for isolatedModules (required for most bundlers)
|
|
226
|
+
if (opts?.isolatedModules !== true) {
|
|
227
|
+
issues.push(
|
|
228
|
+
createIssue({
|
|
229
|
+
id: 'tsconfig-isolated-modules',
|
|
230
|
+
title: 'TSConfig isolatedModules disabled',
|
|
231
|
+
description: `Package "${pkg.name}" does not have "isolatedModules": true`,
|
|
232
|
+
severity: 'info',
|
|
233
|
+
category: 'configuration',
|
|
234
|
+
location: createLocation(tsconfig.filePath),
|
|
235
|
+
suggestion: 'Enable "isolatedModules": true for bundler compatibility',
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check moduleResolution for modern packages
|
|
241
|
+
if (opts?.moduleResolution !== undefined) {
|
|
242
|
+
const modernResolutions = ['node16', 'nodenext', 'bundler']
|
|
243
|
+
const resolution = opts.moduleResolution.toLowerCase()
|
|
244
|
+
|
|
245
|
+
if (!modernResolutions.includes(resolution)) {
|
|
246
|
+
issues.push(
|
|
247
|
+
createIssue({
|
|
248
|
+
id: 'tsconfig-legacy-resolution',
|
|
249
|
+
title: 'TSConfig uses legacy module resolution',
|
|
250
|
+
description: `Package "${pkg.name}" uses "${opts.moduleResolution}" module resolution`,
|
|
251
|
+
severity: 'info',
|
|
252
|
+
category: 'configuration',
|
|
253
|
+
location: createLocation(tsconfig.filePath),
|
|
254
|
+
suggestion: 'Consider using "NodeNext" or "Bundler" for modern module resolution',
|
|
255
|
+
}),
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return issues
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function checkPathMappings(
|
|
264
|
+
pkg: WorkspacePackage,
|
|
265
|
+
tsconfig: ParsedTsConfig,
|
|
266
|
+
allPackages: readonly WorkspacePackage[],
|
|
267
|
+
): Issue[] {
|
|
268
|
+
const issues: Issue[] = []
|
|
269
|
+
const paths = tsconfig.compilerOptions?.paths
|
|
270
|
+
|
|
271
|
+
if (paths === undefined) {
|
|
272
|
+
return issues
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check for path mappings to workspace packages
|
|
276
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
277
|
+
// Check if this looks like a workspace package reference
|
|
278
|
+
if (alias.startsWith('@') && targets.length > 0) {
|
|
279
|
+
const targetPath = targets[0]
|
|
280
|
+
if (targetPath === undefined) {
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Verify the target package exists
|
|
285
|
+
const isWorkspacePackage = allPackages.some(p => {
|
|
286
|
+
const relativePath = path.relative(pkg.packagePath, p.packagePath)
|
|
287
|
+
return targetPath.includes(relativePath) || alias === p.name
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
if (!isWorkspacePackage && !targetPath.includes('node_modules')) {
|
|
291
|
+
issues.push(
|
|
292
|
+
createIssue({
|
|
293
|
+
id: 'tsconfig-invalid-path',
|
|
294
|
+
title: 'TSConfig path mapping may be invalid',
|
|
295
|
+
description: `Package "${pkg.name}" has path mapping "${alias}" -> "${targetPath}" that may not resolve correctly`,
|
|
296
|
+
severity: 'info',
|
|
297
|
+
category: 'configuration',
|
|
298
|
+
location: createLocation(tsconfig.filePath),
|
|
299
|
+
suggestion: 'Verify the path mapping resolves to an existing package or module',
|
|
300
|
+
metadata: {alias, targets},
|
|
301
|
+
}),
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return issues
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function checkReferences(
|
|
311
|
+
pkg: WorkspacePackage,
|
|
312
|
+
tsconfig: ParsedTsConfig,
|
|
313
|
+
allPackages: readonly WorkspacePackage[],
|
|
314
|
+
): Issue[] {
|
|
315
|
+
const issues: Issue[] = []
|
|
316
|
+
|
|
317
|
+
// Get workspace dependencies from package.json
|
|
318
|
+
const deps = {
|
|
319
|
+
...(pkg.packageJson.dependencies ?? {}),
|
|
320
|
+
...(pkg.packageJson.devDependencies ?? {}),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const workspaceDeps = Object.keys(deps).filter(dep => {
|
|
324
|
+
return allPackages.some(p => p.name === dep)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
if (workspaceDeps.length > 0 && tsconfig.references === undefined) {
|
|
328
|
+
issues.push(
|
|
329
|
+
createIssue({
|
|
330
|
+
id: 'tsconfig-missing-references',
|
|
331
|
+
title: 'TSConfig missing project references',
|
|
332
|
+
description: `Package "${pkg.name}" depends on workspace packages but has no project references`,
|
|
333
|
+
severity: 'info',
|
|
334
|
+
category: 'configuration',
|
|
335
|
+
location: createLocation(tsconfig.filePath),
|
|
336
|
+
suggestion: 'Add "references" array for incremental builds',
|
|
337
|
+
metadata: {workspaceDeps},
|
|
338
|
+
}),
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return issues
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function checkCommonIssues(pkg: WorkspacePackage, tsconfig: ParsedTsConfig): Issue[] {
|
|
346
|
+
const issues: Issue[] = []
|
|
347
|
+
const opts = tsconfig.compilerOptions
|
|
348
|
+
|
|
349
|
+
// Check for missing outDir when declaration is true
|
|
350
|
+
if (opts?.declaration === true && opts.outDir === undefined) {
|
|
351
|
+
issues.push(
|
|
352
|
+
createIssue({
|
|
353
|
+
id: 'tsconfig-declaration-no-outdir',
|
|
354
|
+
title: 'TSConfig declaration without outDir',
|
|
355
|
+
description: `Package "${pkg.name}" has "declaration": true but no "outDir" specified`,
|
|
356
|
+
severity: 'warning',
|
|
357
|
+
category: 'configuration',
|
|
358
|
+
location: createLocation(tsconfig.filePath),
|
|
359
|
+
suggestion: 'Add "outDir" to specify where declaration files should be generated',
|
|
360
|
+
}),
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check skipLibCheck (usually recommended for performance)
|
|
365
|
+
if (opts?.skipLibCheck !== true) {
|
|
366
|
+
issues.push(
|
|
367
|
+
createIssue({
|
|
368
|
+
id: 'tsconfig-no-skip-lib-check',
|
|
369
|
+
title: 'TSConfig skipLibCheck disabled',
|
|
370
|
+
description: `Package "${pkg.name}" does not have "skipLibCheck": true`,
|
|
371
|
+
severity: 'info',
|
|
372
|
+
category: 'configuration',
|
|
373
|
+
location: createLocation(tsconfig.filePath),
|
|
374
|
+
suggestion: 'Enable "skipLibCheck": true for faster compilation',
|
|
375
|
+
}),
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return issues
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export {METADATA as tsconfigAnalyzerMetadata}
|