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