@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,229 @@
1
+ /**
2
+ * Configuration merging utilities for workspace analyzer.
3
+ *
4
+ * Provides deep merging of configuration sources with proper precedence:
5
+ * programmatic options > config file > defaults
6
+ */
7
+
8
+ import type {WorkspaceAnalyzerConfig} from './loader'
9
+ import type {WorkspaceAnalyzerConfigOutput} from './schema'
10
+
11
+ import {
12
+ DEFAULT_ANALYZER_CONFIG,
13
+ DEFAULT_CACHE_DIR,
14
+ DEFAULT_CONCURRENCY,
15
+ DEFAULT_HASH_ALGORITHM,
16
+ DEFAULT_MAX_CACHE_AGE,
17
+ DEFAULT_PACKAGE_PATTERNS,
18
+ } from './defaults'
19
+
20
+ /**
21
+ * Complete configuration after merging all sources.
22
+ */
23
+ export interface MergedConfig {
24
+ /** Glob patterns for files to include */
25
+ readonly include: readonly string[]
26
+ /** Glob patterns for files to exclude */
27
+ readonly exclude: readonly string[]
28
+ /** Minimum severity level to report */
29
+ readonly minSeverity: 'info' | 'warning' | 'error' | 'critical'
30
+ /** Categories of issues to check (empty means all) */
31
+ readonly categories: readonly (
32
+ | 'configuration'
33
+ | 'dependency'
34
+ | 'architecture'
35
+ | 'performance'
36
+ | 'circular-import'
37
+ | 'unused-export'
38
+ | 'type-safety'
39
+ )[]
40
+ /** Enable incremental analysis caching */
41
+ readonly cache: boolean
42
+ /** Custom rules configuration */
43
+ readonly rules: Readonly<Record<string, unknown>>
44
+ /** Glob patterns for package locations */
45
+ readonly packagePatterns: readonly string[]
46
+ /** Maximum parallel analysis operations */
47
+ readonly concurrency: number
48
+ /** Directory for analysis cache files */
49
+ readonly cacheDir: string
50
+ /** Maximum cache age in milliseconds */
51
+ readonly maxCacheAge: number
52
+ /** Hash algorithm for file content */
53
+ readonly hashAlgorithm: 'sha256' | 'md5'
54
+ /** Per-analyzer configuration */
55
+ readonly analyzers: Readonly<
56
+ Record<
57
+ string,
58
+ {
59
+ readonly enabled?: boolean
60
+ readonly severity?: 'info' | 'warning' | 'error' | 'critical'
61
+ readonly options?: Readonly<Record<string, unknown>>
62
+ }
63
+ >
64
+ >
65
+ /** Architectural analysis rules */
66
+ readonly architecture?: {
67
+ readonly layers?: readonly {
68
+ readonly name: string
69
+ readonly patterns: readonly string[]
70
+ readonly allowedImports: readonly string[]
71
+ }[]
72
+ readonly allowBarrelExports?: boolean | readonly string[]
73
+ readonly enforcePublicApi?: boolean
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Gets the default merged configuration.
79
+ */
80
+ export function getDefaultMergedConfig(): MergedConfig {
81
+ return {
82
+ include: DEFAULT_ANALYZER_CONFIG.include,
83
+ exclude: DEFAULT_ANALYZER_CONFIG.exclude,
84
+ minSeverity: DEFAULT_ANALYZER_CONFIG.minSeverity,
85
+ categories: DEFAULT_ANALYZER_CONFIG.categories,
86
+ cache: DEFAULT_ANALYZER_CONFIG.cache,
87
+ rules: DEFAULT_ANALYZER_CONFIG.rules,
88
+ packagePatterns: DEFAULT_PACKAGE_PATTERNS,
89
+ concurrency: DEFAULT_CONCURRENCY,
90
+ cacheDir: DEFAULT_CACHE_DIR,
91
+ maxCacheAge: DEFAULT_MAX_CACHE_AGE,
92
+ hashAlgorithm: DEFAULT_HASH_ALGORITHM,
93
+ analyzers: {},
94
+ architecture: undefined,
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Merges analyzer-specific configurations.
100
+ *
101
+ * @param baseAnalyzers - Base analyzer config from file
102
+ * @param overrideAnalyzers - Override config from programmatic options
103
+ * @returns Merged analyzer configurations
104
+ */
105
+ export function mergeAnalyzerConfigs(
106
+ baseAnalyzers: MergedConfig['analyzers'],
107
+ overrideAnalyzers?: WorkspaceAnalyzerConfig['analyzers'],
108
+ ): MergedConfig['analyzers'] {
109
+ if (overrideAnalyzers == null) {
110
+ return baseAnalyzers
111
+ }
112
+
113
+ const merged = {...baseAnalyzers}
114
+
115
+ for (const [id, config] of Object.entries(overrideAnalyzers)) {
116
+ const base = merged[id]
117
+ if (base == null) {
118
+ merged[id] = config
119
+ } else {
120
+ merged[id] = {
121
+ ...base,
122
+ ...config,
123
+ options: config.options == null ? base.options : {...base.options, ...config.options},
124
+ }
125
+ }
126
+ }
127
+
128
+ return merged
129
+ }
130
+
131
+ /**
132
+ * Converts a validated config to a merged config structure.
133
+ */
134
+ function validatedToMerged(config: WorkspaceAnalyzerConfigOutput): MergedConfig {
135
+ return {
136
+ include: config.include,
137
+ exclude: config.exclude,
138
+ minSeverity: config.minSeverity,
139
+ categories: config.categories,
140
+ cache: config.cache,
141
+ rules: config.rules,
142
+ packagePatterns: config.packagePatterns,
143
+ concurrency: config.concurrency,
144
+ cacheDir: config.cacheDir,
145
+ maxCacheAge: config.maxCacheAge,
146
+ hashAlgorithm: config.hashAlgorithm,
147
+ analyzers: config.analyzers,
148
+ architecture: config.architecture,
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Applies partial options to a merged configuration.
154
+ */
155
+ function applyOptions(base: MergedConfig, options: WorkspaceAnalyzerConfig): MergedConfig {
156
+ return {
157
+ include: options.include ?? base.include,
158
+ exclude: options.exclude ?? base.exclude,
159
+ minSeverity: options.minSeverity ?? base.minSeverity,
160
+ categories: options.categories ?? base.categories,
161
+ cache: options.cache ?? base.cache,
162
+ rules: options.rules == null ? base.rules : {...base.rules, ...options.rules},
163
+ packagePatterns: options.packagePatterns ?? base.packagePatterns,
164
+ concurrency: options.concurrency ?? base.concurrency,
165
+ cacheDir: options.cacheDir ?? base.cacheDir,
166
+ maxCacheAge: options.maxCacheAge ?? base.maxCacheAge,
167
+ hashAlgorithm: options.hashAlgorithm ?? base.hashAlgorithm,
168
+ analyzers: mergeAnalyzerConfigs(base.analyzers, options.analyzers),
169
+ architecture: options.architecture ?? base.architecture,
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Merges configuration from multiple sources.
175
+ *
176
+ * Precedence (highest to lowest):
177
+ * 1. Programmatic options passed to analyzeWorkspace()
178
+ * 2. Configuration file (workspace-analyzer.config.ts)
179
+ * 3. Default values
180
+ *
181
+ * @param fileConfig - Configuration loaded from file (optional)
182
+ * @param programmaticOptions - Options passed programmatically (optional)
183
+ * @returns Fully merged configuration with all defaults applied
184
+ */
185
+ export function mergeConfig(
186
+ fileConfig?: WorkspaceAnalyzerConfigOutput,
187
+ programmaticOptions?: WorkspaceAnalyzerConfig,
188
+ ): MergedConfig {
189
+ // Start with defaults
190
+ let merged = getDefaultMergedConfig()
191
+
192
+ // Apply file config if present
193
+ if (fileConfig != null) {
194
+ merged = validatedToMerged(fileConfig)
195
+ }
196
+
197
+ // Apply programmatic options with highest precedence
198
+ if (programmaticOptions != null) {
199
+ merged = applyOptions(merged, programmaticOptions)
200
+ }
201
+
202
+ return merged
203
+ }
204
+
205
+ /**
206
+ * Creates analyzer options from merged configuration for a specific analyzer.
207
+ *
208
+ * @param mergedConfig - The fully merged configuration
209
+ * @param analyzerId - The ID of the analyzer to get options for
210
+ * @returns Options for the specified analyzer
211
+ */
212
+ export function getAnalyzerOptions(
213
+ mergedConfig: MergedConfig,
214
+ analyzerId: string,
215
+ ): {
216
+ enabled: boolean
217
+ minSeverity: MergedConfig['minSeverity']
218
+ categories: MergedConfig['categories']
219
+ options: Record<string, unknown>
220
+ } {
221
+ const analyzerConfig = mergedConfig.analyzers[analyzerId]
222
+
223
+ return {
224
+ enabled: analyzerConfig?.enabled ?? true,
225
+ minSeverity: analyzerConfig?.severity ?? mergedConfig.minSeverity,
226
+ categories: mergedConfig.categories,
227
+ options: (analyzerConfig?.options ?? {}) as Record<string, unknown>,
228
+ }
229
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Zod schemas for configuration validation.
3
+ *
4
+ * Provides runtime validation for workspace analyzer configuration,
5
+ * ensuring type safety and helpful error messages for invalid configs.
6
+ */
7
+
8
+ import {z} from 'zod'
9
+
10
+ import {
11
+ DEFAULT_ANALYZER_CONFIG,
12
+ DEFAULT_CACHE_DIR,
13
+ DEFAULT_CONCURRENCY,
14
+ DEFAULT_MAX_CACHE_AGE,
15
+ DEFAULT_PACKAGE_PATTERNS,
16
+ } from './defaults'
17
+
18
+ /**
19
+ * Valid severity levels for issue filtering.
20
+ */
21
+ export const severitySchema = z.enum(['info', 'warning', 'error', 'critical'])
22
+
23
+ /**
24
+ * Valid issue categories for filtering.
25
+ */
26
+ export const categorySchema = z.enum([
27
+ 'configuration',
28
+ 'dependency',
29
+ 'architecture',
30
+ 'performance',
31
+ 'circular-import',
32
+ 'unused-export',
33
+ 'type-safety',
34
+ ])
35
+
36
+ /**
37
+ * Schema for custom rule configuration.
38
+ */
39
+ export const ruleConfigSchema = z
40
+ .record(z.string(), z.unknown())
41
+ .describe('Custom rule configuration keyed by rule ID')
42
+
43
+ /**
44
+ * Schema for analyzer-specific options.
45
+ */
46
+ export const analyzerOptionsSchema = z
47
+ .object({
48
+ enabled: z.boolean().optional().describe('Whether this analyzer is enabled'),
49
+ severity: severitySchema.optional().describe('Override default severity'),
50
+ options: z.record(z.string(), z.unknown()).optional().describe('Analyzer-specific options'),
51
+ })
52
+ .strict()
53
+
54
+ /**
55
+ * Schema for layer configuration in architectural analysis.
56
+ */
57
+ export const layerConfigSchema = z.object({
58
+ name: z.string().describe('Layer name'),
59
+ patterns: z.array(z.string()).describe('File path patterns matching this layer'),
60
+ allowedImports: z.array(z.string()).describe('Layers this layer can import from'),
61
+ })
62
+
63
+ /**
64
+ * Schema for architectural rules configuration.
65
+ */
66
+ export const architecturalRulesSchema = z
67
+ .object({
68
+ layers: z
69
+ .array(layerConfigSchema)
70
+ .optional()
71
+ .describe('Custom layer definitions for layer violation detection'),
72
+ allowBarrelExports: z
73
+ .union([z.boolean(), z.array(z.string())])
74
+ .optional()
75
+ .describe('Allow barrel exports (true/false or array of allowed paths)'),
76
+ enforcePublicApi: z.boolean().optional().describe('Enforce explicit public API exports'),
77
+ })
78
+ .strict()
79
+
80
+ /**
81
+ * Core analyzer configuration schema.
82
+ */
83
+ export const analyzerConfigSchema = z
84
+ .object({
85
+ include: z
86
+ .array(z.string())
87
+ .optional()
88
+ .default(DEFAULT_ANALYZER_CONFIG.include as unknown as string[])
89
+ .describe('Glob patterns for files to include in analysis'),
90
+ exclude: z
91
+ .array(z.string())
92
+ .optional()
93
+ .default(DEFAULT_ANALYZER_CONFIG.exclude as unknown as string[])
94
+ .describe('Glob patterns for files to exclude from analysis'),
95
+ minSeverity: severitySchema
96
+ .optional()
97
+ .default(DEFAULT_ANALYZER_CONFIG.minSeverity)
98
+ .describe('Minimum severity level to report'),
99
+ categories: z
100
+ .array(categorySchema)
101
+ .optional()
102
+ .default([])
103
+ .describe('Categories of issues to check (empty means all)'),
104
+ cache: z.boolean().optional().default(true).describe('Enable incremental analysis caching'),
105
+ rules: ruleConfigSchema.optional().default({}).describe('Custom rules configuration'),
106
+ })
107
+ .strict()
108
+
109
+ /**
110
+ * Workspace-specific options schema.
111
+ */
112
+ export const workspaceOptionsSchema = z
113
+ .object({
114
+ packagePatterns: z
115
+ .array(z.string())
116
+ .optional()
117
+ .default(DEFAULT_PACKAGE_PATTERNS as unknown as string[])
118
+ .describe('Glob patterns for package locations'),
119
+ concurrency: z
120
+ .number()
121
+ .int()
122
+ .min(1)
123
+ .max(16)
124
+ .optional()
125
+ .default(DEFAULT_CONCURRENCY)
126
+ .describe('Maximum parallel analysis operations'),
127
+ cacheDir: z
128
+ .string()
129
+ .optional()
130
+ .default(DEFAULT_CACHE_DIR)
131
+ .describe('Directory for analysis cache files'),
132
+ maxCacheAge: z
133
+ .number()
134
+ .int()
135
+ .min(0)
136
+ .optional()
137
+ .default(DEFAULT_MAX_CACHE_AGE)
138
+ .describe('Maximum cache age in milliseconds'),
139
+ hashAlgorithm: z
140
+ .enum(['sha256', 'md5'])
141
+ .optional()
142
+ .default('sha256')
143
+ .describe('Hash algorithm for file content'),
144
+ })
145
+ .strict()
146
+
147
+ /**
148
+ * Per-analyzer configuration in the config file.
149
+ */
150
+ export const analyzersConfigSchema = z
151
+ .record(z.string(), analyzerOptionsSchema)
152
+ .optional()
153
+ .default({})
154
+ .describe('Per-analyzer configuration overrides')
155
+
156
+ /**
157
+ * Complete workspace analyzer configuration schema.
158
+ */
159
+ export const workspaceAnalyzerConfigSchema = z
160
+ .object({
161
+ // Extend from core analyzer config
162
+ ...analyzerConfigSchema.shape,
163
+ // Add workspace-specific options
164
+ ...workspaceOptionsSchema.shape,
165
+ // Per-analyzer configuration
166
+ analyzers: analyzersConfigSchema,
167
+ // Architectural rules
168
+ architecture: architecturalRulesSchema.optional().describe('Architectural analysis rules'),
169
+ })
170
+ .strict()
171
+
172
+ /**
173
+ * Schema for the analyzeWorkspace() options parameter.
174
+ */
175
+ export const analyzeWorkspaceOptionsSchema = z
176
+ .object({
177
+ // Include base config options
178
+ ...analyzerConfigSchema.shape,
179
+ // Include workspace options
180
+ ...workspaceOptionsSchema.shape,
181
+ // API-specific options
182
+ configPath: z.string().optional().describe('Path to workspace-analyzer.config.ts file'),
183
+ // onProgress is validated at runtime, not in schema (function types not well-supported in zod v4)
184
+ analyzers: analyzersConfigSchema,
185
+ architecture: architecturalRulesSchema.optional(),
186
+ })
187
+ .strict()
188
+
189
+ /**
190
+ * Inferred types from schemas.
191
+ */
192
+ export type SeverityInput = z.input<typeof severitySchema>
193
+ export type CategoryInput = z.input<typeof categorySchema>
194
+ export type AnalyzerConfigInput = z.input<typeof analyzerConfigSchema>
195
+ export type WorkspaceOptionsInput = z.input<typeof workspaceOptionsSchema>
196
+ export type WorkspaceAnalyzerConfigInput = z.input<typeof workspaceAnalyzerConfigSchema>
197
+ export type AnalyzeWorkspaceOptionsInput = z.input<typeof analyzeWorkspaceOptionsSchema>
198
+
199
+ export type SeverityOutput = z.output<typeof severitySchema>
200
+ export type CategoryOutput = z.output<typeof categorySchema>
201
+ export type AnalyzerConfigOutput = z.output<typeof analyzerConfigSchema>
202
+ export type WorkspaceOptionsOutput = z.output<typeof workspaceOptionsSchema>
203
+ export type WorkspaceAnalyzerConfigOutput = z.output<typeof workspaceAnalyzerConfigSchema>
204
+ export type AnalyzeWorkspaceOptionsOutput = z.output<typeof analyzeWorkspaceOptionsSchema>
205
+
206
+ /**
207
+ * Validates and parses analyzer configuration.
208
+ *
209
+ * @param config - Raw configuration input
210
+ * @returns Validated and defaulted configuration
211
+ * @throws {z.ZodError} If validation fails
212
+ */
213
+ export function parseAnalyzerConfig(config: unknown): AnalyzerConfigOutput {
214
+ return analyzerConfigSchema.parse(config)
215
+ }
216
+
217
+ /**
218
+ * Validates and parses workspace analyzer configuration.
219
+ *
220
+ * @param config - Raw configuration input
221
+ * @returns Validated and defaulted configuration
222
+ * @throws {z.ZodError} If validation fails
223
+ */
224
+ export function parseWorkspaceAnalyzerConfig(config: unknown): WorkspaceAnalyzerConfigOutput {
225
+ return workspaceAnalyzerConfigSchema.parse(config)
226
+ }
227
+
228
+ /**
229
+ * Safe parse result type for configuration validation.
230
+ */
231
+ export type SafeParseResult<T> = {success: true; data: T} | {success: false; error: z.ZodError}
232
+
233
+ /**
234
+ * Safely validates configuration without throwing.
235
+ *
236
+ * @param config - Raw configuration input
237
+ * @returns Validation result with success status and data/error
238
+ */
239
+ export function safeParseWorkspaceAnalyzerConfig(
240
+ config: unknown,
241
+ ): SafeParseResult<WorkspaceAnalyzerConfigOutput> {
242
+ const result = workspaceAnalyzerConfigSchema.safeParse(config)
243
+ if (result.success) {
244
+ return {success: true, data: result.data}
245
+ }
246
+ return {success: false, error: result.error}
247
+ }
248
+
249
+ /**
250
+ * Validates analyzeWorkspace() options.
251
+ *
252
+ * @param options - Raw options input
253
+ * @returns Validation result with success status and data/error
254
+ */
255
+ export function safeParseAnalyzeOptions(
256
+ options: unknown,
257
+ ): SafeParseResult<AnalyzeWorkspaceOptionsOutput> {
258
+ const result = analyzeWorkspaceOptionsSchema.safeParse(options)
259
+ if (result.success) {
260
+ return {success: true, data: result.data}
261
+ }
262
+ return {success: false, error: result.error}
263
+ }