@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,52 @@
1
+ /**
2
+ * Rules module exports for architectural analysis.
3
+ *
4
+ * Provides the rule engine infrastructure and built-in rules for detecting
5
+ * architectural anti-patterns and enforcing best practices.
6
+ */
7
+
8
+ export type {
9
+ BarrelExportRuleOptions,
10
+ LayerViolationRuleOptions,
11
+ PackageBoundaryRuleOptions,
12
+ PathAliasRuleOptions,
13
+ PublicApiRuleOptions,
14
+ SideEffectRuleOptions,
15
+ } from './builtin-rules'
16
+ export {
17
+ barrelExportRuleMetadata,
18
+ createBarrelExportRule,
19
+ createLayerViolationRule,
20
+ createPackageBoundaryRule,
21
+ createPathAliasRule,
22
+ createPublicApiRule,
23
+ createSideEffectRule,
24
+ layerViolationRuleMetadata,
25
+ packageBoundaryRuleMetadata,
26
+ pathAliasRuleMetadata,
27
+ publicApiRuleMetadata,
28
+ sideEffectRuleMetadata,
29
+ } from './builtin-rules'
30
+
31
+ export type {
32
+ LayerConfiguration,
33
+ LayerDefinition,
34
+ LayerPattern,
35
+ Rule,
36
+ RuleContext,
37
+ RuleEngine,
38
+ RuleEngineError,
39
+ RuleFactory,
40
+ RuleMetadata,
41
+ RuleOptions,
42
+ RuleRegistration,
43
+ RuleResult,
44
+ RuleViolation,
45
+ } from './rule-engine'
46
+ export {
47
+ BUILTIN_RULE_IDS,
48
+ createRuleEngine,
49
+ DEFAULT_LAYER_CONFIG,
50
+ getFileLayer,
51
+ isLayerImportAllowed,
52
+ } from './rule-engine'
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Architectural rule engine for validating code patterns and enforcing best practices.
3
+ *
4
+ * Provides a plugin architecture for extensible analysis rules that detect
5
+ * architectural violations, anti-patterns, and best practice deviations.
6
+ */
7
+
8
+ import type {SourceFile} from 'ts-morph'
9
+
10
+ import type {WorkspacePackage} from '../scanner/workspace-scanner'
11
+ import type {Issue, IssueLocation, Severity} from '../types/index'
12
+ import type {Result} from '../types/result'
13
+
14
+ import {ok} from '@bfra.me/es/result'
15
+
16
+ import {matchPattern, normalizePath} from '../utils/pattern-matcher'
17
+
18
+ /**
19
+ * Context provided to rules during evaluation.
20
+ */
21
+ export interface RuleContext {
22
+ /** The source file being evaluated */
23
+ readonly sourceFile: SourceFile
24
+ /** The package containing the source file */
25
+ readonly pkg: WorkspacePackage
26
+ /** Root path of the workspace */
27
+ readonly workspacePath: string
28
+ /** All packages in the workspace for cross-package analysis */
29
+ readonly allPackages: readonly WorkspacePackage[]
30
+ /** Resolved tsconfig paths for alias validation */
31
+ readonly tsconfigPaths?: Readonly<Record<string, readonly string[]>>
32
+ /** Layer configuration for architectural validation */
33
+ readonly layerConfig?: LayerConfiguration
34
+ /** Report progress during evaluation */
35
+ readonly reportProgress?: (message: string) => void
36
+ }
37
+
38
+ /**
39
+ * Layer configuration for enforcing architectural boundaries.
40
+ */
41
+ export interface LayerConfiguration {
42
+ /** Layer definitions with allowed dependencies */
43
+ readonly layers: readonly LayerDefinition[]
44
+ /** File patterns to layer mapping */
45
+ readonly patterns: readonly LayerPattern[]
46
+ }
47
+
48
+ /**
49
+ * Defines an architectural layer with its allowed dependencies.
50
+ */
51
+ export interface LayerDefinition {
52
+ /** Layer name (e.g., 'domain', 'application', 'infrastructure') */
53
+ readonly name: string
54
+ /** Other layers this layer can import from */
55
+ readonly allowedDependencies: readonly string[]
56
+ }
57
+
58
+ /**
59
+ * Maps file patterns to architectural layers.
60
+ */
61
+ export interface LayerPattern {
62
+ /** Glob pattern to match files */
63
+ readonly pattern: string
64
+ /** Layer this pattern belongs to */
65
+ readonly layer: string
66
+ }
67
+
68
+ /**
69
+ * Result of a single rule violation.
70
+ */
71
+ export interface RuleViolation {
72
+ /** Rule that was violated */
73
+ readonly ruleId: string
74
+ /** Location of the violation */
75
+ readonly location: IssueLocation
76
+ /** Human-readable message explaining the violation */
77
+ readonly message: string
78
+ /** Suggested fix for the violation */
79
+ readonly suggestion?: string
80
+ /** Related locations (e.g., the imported module) */
81
+ readonly relatedLocations?: readonly IssueLocation[]
82
+ /** Additional metadata for machine processing */
83
+ readonly metadata?: Readonly<Record<string, unknown>>
84
+ }
85
+
86
+ /**
87
+ * Result of evaluating a rule on a source file.
88
+ */
89
+ export interface RuleResult {
90
+ /** Violations found */
91
+ readonly violations: readonly RuleViolation[]
92
+ /** Whether the evaluation completed successfully */
93
+ readonly success: boolean
94
+ /** Error message if evaluation failed */
95
+ readonly error?: string
96
+ }
97
+
98
+ /**
99
+ * Metadata describing a rule.
100
+ */
101
+ export interface RuleMetadata {
102
+ /** Unique identifier for the rule */
103
+ readonly id: string
104
+ /** Human-readable name */
105
+ readonly name: string
106
+ /** Description of what the rule checks */
107
+ readonly description: string
108
+ /** Default severity for violations */
109
+ readonly defaultSeverity: Severity
110
+ /** Category for grouping */
111
+ readonly category: 'layer-violation' | 'barrel-export' | 'public-api' | 'side-effect' | 'boundary'
112
+ /** Documentation URL */
113
+ readonly docsUrl?: string
114
+ }
115
+
116
+ /**
117
+ * Configuration options for a rule.
118
+ */
119
+ export interface RuleOptions {
120
+ /** Whether the rule is enabled */
121
+ readonly enabled?: boolean
122
+ /** Severity override */
123
+ readonly severity?: Severity
124
+ /** File patterns to include */
125
+ readonly include?: readonly string[]
126
+ /** File patterns to exclude */
127
+ readonly exclude?: readonly string[]
128
+ /** Rule-specific options */
129
+ readonly options?: Readonly<Record<string, unknown>>
130
+ }
131
+
132
+ /**
133
+ * Core interface that all architectural rules must implement.
134
+ */
135
+ export interface Rule {
136
+ /** Metadata describing the rule */
137
+ readonly metadata: RuleMetadata
138
+ /**
139
+ * Evaluate the rule against a source file.
140
+ *
141
+ * @param context - Rule evaluation context
142
+ * @returns Result containing violations found
143
+ */
144
+ readonly evaluate: (context: RuleContext) => Promise<RuleResult>
145
+ }
146
+
147
+ /**
148
+ * Factory function signature for creating rules.
149
+ */
150
+ export type RuleFactory = (options?: RuleOptions) => Rule
151
+
152
+ /**
153
+ * Registration entry for a rule in the engine.
154
+ */
155
+ export interface RuleRegistration {
156
+ /** The rule instance or factory */
157
+ readonly rule: Rule | RuleFactory
158
+ /** Whether the rule is enabled */
159
+ readonly enabled: boolean
160
+ /** Priority for execution order (lower runs first) */
161
+ readonly priority: number
162
+ /** Configuration options */
163
+ readonly options?: RuleOptions
164
+ }
165
+
166
+ /**
167
+ * Rule engine for managing and executing architectural rules.
168
+ */
169
+ export interface RuleEngine {
170
+ /** Register a rule */
171
+ readonly register: (id: string, registration: RuleRegistration) => void
172
+ /** Unregister a rule */
173
+ readonly unregister: (id: string) => boolean
174
+ /** Get a registered rule */
175
+ readonly get: (id: string) => RuleRegistration | undefined
176
+ /** Get all registered rules */
177
+ readonly getAll: () => Map<string, RuleRegistration>
178
+ /** Get enabled rules sorted by priority */
179
+ readonly getEnabled: () => Rule[]
180
+ /** Check if a rule is registered */
181
+ readonly has: (id: string) => boolean
182
+ /** Evaluate all enabled rules against a source file */
183
+ readonly evaluateFile: (
184
+ context: RuleContext,
185
+ ) => Promise<Result<readonly Issue[], RuleEngineError>>
186
+ /** Evaluate all enabled rules against multiple source files */
187
+ readonly evaluateFiles: (
188
+ contexts: readonly RuleContext[],
189
+ ) => Promise<Result<readonly Issue[], RuleEngineError>>
190
+ }
191
+
192
+ /**
193
+ * Error from rule engine operations.
194
+ */
195
+ export interface RuleEngineError {
196
+ /** Error code for programmatic handling */
197
+ readonly code: 'RULE_EXECUTION_ERROR' | 'RULE_NOT_FOUND' | 'INVALID_CONFIGURATION'
198
+ /** Human-readable error message */
199
+ readonly message: string
200
+ /** Rule ID that caused the error (if applicable) */
201
+ readonly ruleId?: string
202
+ /** Additional context about the error */
203
+ readonly context?: Readonly<Record<string, unknown>>
204
+ }
205
+
206
+ /**
207
+ * Resolves a rule registration to a Rule instance.
208
+ */
209
+ function resolveRule(registration: RuleRegistration): Rule {
210
+ if (typeof registration.rule === 'function') {
211
+ return registration.rule(registration.options)
212
+ }
213
+ return registration.rule
214
+ }
215
+
216
+ /**
217
+ * Converts a rule violation to an Issue.
218
+ */
219
+ function violationToIssue(violation: RuleViolation, rule: Rule): Issue {
220
+ return {
221
+ id: violation.ruleId,
222
+ title: `${rule.metadata.name}: ${violation.message.slice(0, 60)}${violation.message.length > 60 ? '...' : ''}`,
223
+ description: violation.message,
224
+ severity: rule.metadata.defaultSeverity,
225
+ category: 'architecture',
226
+ location: violation.location,
227
+ relatedLocations: violation.relatedLocations,
228
+ suggestion: violation.suggestion,
229
+ metadata: violation.metadata,
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Creates a new rule engine instance.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * const engine = createRuleEngine()
239
+ *
240
+ * engine.register('no-barrel-exports', {
241
+ * rule: createBarrelExportRule(),
242
+ * enabled: true,
243
+ * priority: 10,
244
+ * })
245
+ *
246
+ * const issues = await engine.evaluateFile(context)
247
+ * ```
248
+ */
249
+ export function createRuleEngine(): RuleEngine {
250
+ const registrations = new Map<string, RuleRegistration>()
251
+
252
+ return {
253
+ register(id: string, registration: RuleRegistration): void {
254
+ registrations.set(id, registration)
255
+ },
256
+
257
+ unregister(id: string): boolean {
258
+ return registrations.delete(id)
259
+ },
260
+
261
+ get(id: string): RuleRegistration | undefined {
262
+ return registrations.get(id)
263
+ },
264
+
265
+ getAll(): Map<string, RuleRegistration> {
266
+ return new Map(registrations)
267
+ },
268
+
269
+ getEnabled(): Rule[] {
270
+ return Array.from(registrations.entries())
271
+ .filter(([, reg]) => reg.enabled)
272
+ .sort((a, b) => a[1].priority - b[1].priority)
273
+ .map(([, reg]) => resolveRule(reg))
274
+ },
275
+
276
+ has(id: string): boolean {
277
+ return registrations.has(id)
278
+ },
279
+
280
+ async evaluateFile(context: RuleContext): Promise<Result<readonly Issue[], RuleEngineError>> {
281
+ const issues: Issue[] = []
282
+ const enabledRules = this.getEnabled()
283
+
284
+ for (const rule of enabledRules) {
285
+ try {
286
+ const result = await rule.evaluate(context)
287
+ if (result.success) {
288
+ for (const violation of result.violations) {
289
+ issues.push(violationToIssue(violation, rule))
290
+ }
291
+ }
292
+ } catch (error) {
293
+ return {
294
+ success: false,
295
+ error: {
296
+ code: 'RULE_EXECUTION_ERROR',
297
+ message: `Rule ${rule.metadata.id} threw an error: ${error instanceof Error ? error.message : String(error)}`,
298
+ ruleId: rule.metadata.id,
299
+ },
300
+ }
301
+ }
302
+ }
303
+
304
+ return ok(issues)
305
+ },
306
+
307
+ async evaluateFiles(
308
+ contexts: readonly RuleContext[],
309
+ ): Promise<Result<readonly Issue[], RuleEngineError>> {
310
+ const allIssues: Issue[] = []
311
+
312
+ for (const context of contexts) {
313
+ const result = await this.evaluateFile(context)
314
+ if (!result.success) {
315
+ return result
316
+ }
317
+ allIssues.push(...result.data)
318
+ }
319
+
320
+ return ok(allIssues)
321
+ },
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Default layer configuration for typical TypeScript projects.
327
+ */
328
+ export const DEFAULT_LAYER_CONFIG: LayerConfiguration = {
329
+ layers: [
330
+ {name: 'domain', allowedDependencies: []},
331
+ {name: 'application', allowedDependencies: ['domain']},
332
+ {name: 'infrastructure', allowedDependencies: ['domain', 'application']},
333
+ {name: 'presentation', allowedDependencies: ['domain', 'application']},
334
+ {name: 'shared', allowedDependencies: []},
335
+ ],
336
+ patterns: [
337
+ {pattern: '**/domain/**', layer: 'domain'},
338
+ {pattern: '**/models/**', layer: 'domain'},
339
+ {pattern: '**/entities/**', layer: 'domain'},
340
+ {pattern: '**/application/**', layer: 'application'},
341
+ {pattern: '**/services/**', layer: 'application'},
342
+ {pattern: '**/use-cases/**', layer: 'application'},
343
+ {pattern: '**/infrastructure/**', layer: 'infrastructure'},
344
+ {pattern: '**/adapters/**', layer: 'infrastructure'},
345
+ {pattern: '**/repositories/**', layer: 'infrastructure'},
346
+ {pattern: '**/presentation/**', layer: 'presentation'},
347
+ {pattern: '**/ui/**', layer: 'presentation'},
348
+ {pattern: '**/components/**', layer: 'presentation'},
349
+ {pattern: '**/shared/**', layer: 'shared'},
350
+ {pattern: '**/utils/**', layer: 'shared'},
351
+ {pattern: '**/lib/**', layer: 'shared'},
352
+ ],
353
+ }
354
+
355
+ /**
356
+ * Determines the architectural layer for a file path based on patterns.
357
+ *
358
+ * @param filePath - File path to check
359
+ * @param config - Layer configuration
360
+ * @returns Layer name or undefined if no match
361
+ */
362
+ export function getFileLayer(filePath: string, config: LayerConfiguration): string | undefined {
363
+ const normalizedPath = normalizePath(filePath)
364
+
365
+ for (const {pattern, layer} of config.patterns) {
366
+ if (matchPattern(normalizedPath, pattern)) {
367
+ return layer
368
+ }
369
+ }
370
+
371
+ return undefined
372
+ }
373
+
374
+ /**
375
+ * Checks if an import from one layer to another is allowed.
376
+ *
377
+ * @param sourceLayer - Layer of the importing file
378
+ * @param targetLayer - Layer of the imported module
379
+ * @param config - Layer configuration
380
+ * @returns Whether the import is allowed
381
+ */
382
+ export function isLayerImportAllowed(
383
+ sourceLayer: string,
384
+ targetLayer: string,
385
+ config: LayerConfiguration,
386
+ ): boolean {
387
+ if (sourceLayer === targetLayer) {
388
+ return true
389
+ }
390
+
391
+ const sourceLayerDef = config.layers.find(l => l.name === sourceLayer)
392
+ if (sourceLayerDef === undefined) {
393
+ return true
394
+ }
395
+
396
+ return sourceLayerDef.allowedDependencies.includes(targetLayer)
397
+ }
398
+
399
+ /**
400
+ * Built-in rule IDs.
401
+ */
402
+ export const BUILTIN_RULE_IDS = {
403
+ LAYER_VIOLATION: 'layer-violation',
404
+ BARREL_EXPORT: 'barrel-export',
405
+ PUBLIC_API: 'public-api',
406
+ SIDE_EFFECT: 'side-effect',
407
+ PATH_ALIAS: 'path-alias',
408
+ PACKAGE_BOUNDARY: 'package-boundary',
409
+ } as const
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Workspace scanning module exports.
3
+ */
4
+
5
+ export {
6
+ createWorkspaceScanner,
7
+ filterPackagesByPattern,
8
+ getPackageScope,
9
+ getUnscopedName,
10
+ groupPackagesByScope,
11
+ } from './workspace-scanner'
12
+ export type {
13
+ ScanError,
14
+ WorkspacePackage,
15
+ WorkspacePackageJson,
16
+ WorkspaceScannerOptions,
17
+ WorkspaceScanResult,
18
+ } from './workspace-scanner'