@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,444 @@
1
+ /**
2
+ * Bundle size estimator for identifying large module contributors.
3
+ *
4
+ * Analyzes source files and dependencies to estimate bundle size impact,
5
+ * helping identify optimization opportunities for tree-shaking and code splitting.
6
+ *
7
+ * This is a heuristic estimator - actual bundle sizes depend on bundler configuration,
8
+ * tree-shaking effectiveness, and minification.
9
+ */
10
+
11
+ import type {ExtractedImport, ImportExtractionResult} from '../parser/import-extractor'
12
+ import type {WorkspacePackage} from '../scanner/workspace-scanner'
13
+
14
+ import fs from 'node:fs/promises'
15
+
16
+ /**
17
+ * Estimated size information for a module or package.
18
+ */
19
+ export interface BundleSizeEstimate {
20
+ /** Source file path or package name */
21
+ readonly identifier: string
22
+ /** Raw source size in bytes (unminified) */
23
+ readonly sourceSize: number
24
+ /** Estimated minified size in bytes (heuristic: ~60% of source) */
25
+ readonly estimatedMinifiedSize: number
26
+ /** Estimated gzipped size in bytes (heuristic: ~30% of minified) */
27
+ readonly estimatedGzippedSize: number
28
+ /** Number of imports this module brings in */
29
+ readonly importCount: number
30
+ /** Number of exports this module provides */
31
+ readonly exportCount: number
32
+ /** Whether this is a direct or transitive dependency */
33
+ readonly isTransitive: boolean
34
+ }
35
+
36
+ /**
37
+ * Aggregated bundle size statistics for a package.
38
+ */
39
+ export interface PackageBundleStats {
40
+ /** Package name */
41
+ readonly packageName: string
42
+ /** Total source size of all modules */
43
+ readonly totalSourceSize: number
44
+ /** Estimated total minified size */
45
+ readonly totalMinifiedSize: number
46
+ /** Estimated total gzipped size */
47
+ readonly totalGzippedSize: number
48
+ /** Number of source files */
49
+ readonly fileCount: number
50
+ /** Individual file estimates */
51
+ readonly files: readonly BundleSizeEstimate[]
52
+ /** External dependency size estimates */
53
+ readonly dependencies: readonly DependencySizeEstimate[]
54
+ /** Top contributors by size */
55
+ readonly topContributors: readonly BundleSizeEstimate[]
56
+ }
57
+
58
+ /**
59
+ * Size estimate for an external dependency.
60
+ */
61
+ export interface DependencySizeEstimate {
62
+ /** Package name */
63
+ readonly packageName: string
64
+ /** Estimated size (if known from registry data) */
65
+ readonly estimatedSize?: number
66
+ /** Whether size data is available */
67
+ readonly sizeKnown: boolean
68
+ /** Number of times imported */
69
+ readonly importCount: number
70
+ /** Import locations */
71
+ readonly locations: readonly string[]
72
+ }
73
+
74
+ /**
75
+ * Options for bundle size estimation.
76
+ */
77
+ export interface BundleEstimatorOptions {
78
+ /** Include node_modules size estimates */
79
+ readonly includeNodeModules?: boolean
80
+ /** Maximum files to analyze (for performance) */
81
+ readonly maxFiles?: number
82
+ /** Minification ratio estimate (0-1) */
83
+ readonly minificationRatio?: number
84
+ /** Gzip ratio estimate (0-1) */
85
+ readonly gzipRatio?: number
86
+ /** Size threshold for "large file" warnings (bytes) */
87
+ readonly largeFileThreshold?: number
88
+ /** Size threshold for "large dependency" warnings (bytes) */
89
+ readonly largeDependencyThreshold?: number
90
+ }
91
+
92
+ const DEFAULT_OPTIONS: Required<BundleEstimatorOptions> = {
93
+ includeNodeModules: false,
94
+ maxFiles: 10000,
95
+ minificationRatio: 0.6,
96
+ gzipRatio: 0.3,
97
+ largeFileThreshold: 50000,
98
+ largeDependencyThreshold: 100000,
99
+ }
100
+
101
+ // Known large package sizes (rough estimates in KB, gzipped)
102
+ const KNOWN_PACKAGE_SIZES: Readonly<Record<string, number>> = {
103
+ lodash: 71,
104
+ 'lodash-es': 71,
105
+ moment: 67,
106
+ 'moment-timezone': 95,
107
+ rxjs: 40,
108
+ '@angular/core': 90,
109
+ '@angular/common': 45,
110
+ react: 6,
111
+ 'react-dom': 42,
112
+ vue: 34,
113
+ d3: 80,
114
+ 'chart.js': 65,
115
+ three: 150,
116
+ '@mui/material': 120,
117
+ antd: 200,
118
+ 'date-fns': 25,
119
+ dayjs: 3,
120
+ axios: 13,
121
+ zod: 12,
122
+ yup: 22,
123
+ 'class-validator': 15,
124
+ typeorm: 180,
125
+ prisma: 40,
126
+ '@prisma/client': 40,
127
+ express: 30,
128
+ fastify: 25,
129
+ 'ts-morph': 150,
130
+ typescript: 150,
131
+ }
132
+
133
+ /**
134
+ * Estimates bundle size for source files in a package.
135
+ */
136
+ export async function estimatePackageBundleSize(
137
+ pkg: WorkspacePackage,
138
+ importResults: readonly ImportExtractionResult[],
139
+ options?: BundleEstimatorOptions,
140
+ ): Promise<PackageBundleStats> {
141
+ const opts = {...DEFAULT_OPTIONS, ...options}
142
+
143
+ const fileEstimates: BundleSizeEstimate[] = []
144
+ let totalSourceSize = 0
145
+ let totalMinifiedSize = 0
146
+ let totalGzippedSize = 0
147
+
148
+ const filesToAnalyze = pkg.sourceFiles.slice(0, opts.maxFiles)
149
+
150
+ for (const filePath of filesToAnalyze) {
151
+ try {
152
+ const stats = await fs.stat(filePath)
153
+ const sourceSize = stats.size
154
+
155
+ const fileImports = importResults.find(r => r.filePath === filePath)
156
+ const importCount = fileImports?.imports.length ?? 0
157
+
158
+ const estimate = createSizeEstimate(
159
+ filePath,
160
+ sourceSize,
161
+ importCount,
162
+ 0,
163
+ false,
164
+ opts.minificationRatio,
165
+ opts.gzipRatio,
166
+ )
167
+
168
+ fileEstimates.push(estimate)
169
+ totalSourceSize += sourceSize
170
+ totalMinifiedSize += estimate.estimatedMinifiedSize
171
+ totalGzippedSize += estimate.estimatedGzippedSize
172
+ } catch {
173
+ // File may not exist or be inaccessible
174
+ }
175
+ }
176
+
177
+ const dependencyEstimates = collectDependencyEstimates(importResults, opts)
178
+
179
+ const topContributors = [...fileEstimates]
180
+ .sort((a, b) => b.sourceSize - a.sourceSize)
181
+ .slice(0, 10)
182
+
183
+ return {
184
+ packageName: pkg.name,
185
+ totalSourceSize,
186
+ totalMinifiedSize,
187
+ totalGzippedSize,
188
+ fileCount: fileEstimates.length,
189
+ files: fileEstimates,
190
+ dependencies: dependencyEstimates,
191
+ topContributors,
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Estimates the size of a single source file.
197
+ */
198
+ export async function estimateFileSize(
199
+ filePath: string,
200
+ options?: BundleEstimatorOptions,
201
+ ): Promise<BundleSizeEstimate | null> {
202
+ const opts = {...DEFAULT_OPTIONS, ...options}
203
+
204
+ try {
205
+ const stats = await fs.stat(filePath)
206
+ return createSizeEstimate(
207
+ filePath,
208
+ stats.size,
209
+ 0,
210
+ 0,
211
+ false,
212
+ opts.minificationRatio,
213
+ opts.gzipRatio,
214
+ )
215
+ } catch {
216
+ return null
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Estimates the size contribution of an external dependency.
222
+ */
223
+ export function estimateDependencySize(packageName: string): DependencySizeEstimate {
224
+ const baseName = getBasePackageName(packageName)
225
+ const knownSize = KNOWN_PACKAGE_SIZES[baseName]
226
+
227
+ if (knownSize === undefined) {
228
+ return {
229
+ packageName,
230
+ estimatedSize: undefined,
231
+ sizeKnown: false,
232
+ importCount: 0,
233
+ locations: [],
234
+ }
235
+ }
236
+
237
+ return {
238
+ packageName,
239
+ estimatedSize: knownSize * 1024,
240
+ sizeKnown: true,
241
+ importCount: 0,
242
+ locations: [],
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Identifies files that exceed the large file threshold.
248
+ */
249
+ export function findLargeFiles(
250
+ stats: PackageBundleStats,
251
+ threshold?: number,
252
+ ): readonly BundleSizeEstimate[] {
253
+ const actualThreshold = threshold ?? DEFAULT_OPTIONS.largeFileThreshold
254
+ return stats.files.filter(file => file.sourceSize > actualThreshold)
255
+ }
256
+
257
+ /**
258
+ * Identifies dependencies that exceed the large dependency threshold.
259
+ */
260
+ export function findLargeDependencies(
261
+ stats: PackageBundleStats,
262
+ threshold?: number,
263
+ ): readonly DependencySizeEstimate[] {
264
+ const actualThreshold = threshold ?? DEFAULT_OPTIONS.largeDependencyThreshold
265
+ return stats.dependencies.filter(
266
+ dep => dep.sizeKnown && dep.estimatedSize !== undefined && dep.estimatedSize > actualThreshold,
267
+ )
268
+ }
269
+
270
+ /**
271
+ * Calculates the estimated tree-shaking savings for a file.
272
+ *
273
+ * Files that only use a subset of exports from large modules
274
+ * can benefit significantly from tree-shaking.
275
+ */
276
+ export function estimateTreeShakingSavings(
277
+ imports: readonly ExtractedImport[],
278
+ ): TreeShakingSavingsEstimate {
279
+ let potentialSavings = 0
280
+ const optimizableImports: OptimizableImport[] = []
281
+
282
+ for (const imp of imports) {
283
+ if (imp.namespaceImport !== undefined) {
284
+ // Namespace imports (import * as X) prevent tree-shaking
285
+ const baseName = getBasePackageName(imp.moduleSpecifier)
286
+ const knownSize = KNOWN_PACKAGE_SIZES[baseName]
287
+
288
+ if (knownSize !== undefined) {
289
+ // Estimate potential savings (heuristic: 50% of package)
290
+ const savings = Math.floor(knownSize * 1024 * 0.5)
291
+ potentialSavings += savings
292
+
293
+ optimizableImports.push({
294
+ moduleSpecifier: imp.moduleSpecifier,
295
+ currentImportStyle: 'namespace',
296
+ suggestedImportStyle: 'named',
297
+ estimatedSavings: savings,
298
+ line: imp.line,
299
+ column: imp.column,
300
+ })
301
+ }
302
+ }
303
+
304
+ if (imp.defaultImport !== undefined && imp.importedNames === undefined) {
305
+ // Default-only imports from packages with named exports
306
+ const baseName = getBasePackageName(imp.moduleSpecifier)
307
+ const knownSize = KNOWN_PACKAGE_SIZES[baseName]
308
+
309
+ if (knownSize !== undefined && !imp.isRelative) {
310
+ const savings = Math.floor(knownSize * 1024 * 0.3)
311
+ potentialSavings += savings
312
+
313
+ optimizableImports.push({
314
+ moduleSpecifier: imp.moduleSpecifier,
315
+ currentImportStyle: 'default',
316
+ suggestedImportStyle: 'named',
317
+ estimatedSavings: savings,
318
+ line: imp.line,
319
+ column: imp.column,
320
+ })
321
+ }
322
+ }
323
+ }
324
+
325
+ return {
326
+ potentialSavings,
327
+ optimizableImports,
328
+ hasPotentialOptimizations: optimizableImports.length > 0,
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Estimate of potential tree-shaking savings.
334
+ */
335
+ export interface TreeShakingSavingsEstimate {
336
+ /** Total potential bytes saved */
337
+ readonly potentialSavings: number
338
+ /** Imports that could be optimized */
339
+ readonly optimizableImports: readonly OptimizableImport[]
340
+ /** Whether any optimizations are available */
341
+ readonly hasPotentialOptimizations: boolean
342
+ }
343
+
344
+ /**
345
+ * An import that could be optimized for better tree-shaking.
346
+ */
347
+ export interface OptimizableImport {
348
+ /** The module specifier */
349
+ readonly moduleSpecifier: string
350
+ /** Current import style */
351
+ readonly currentImportStyle: 'namespace' | 'default' | 'side-effect'
352
+ /** Suggested import style */
353
+ readonly suggestedImportStyle: 'named' | 'dynamic'
354
+ /** Estimated bytes saved */
355
+ readonly estimatedSavings: number
356
+ /** Line number */
357
+ readonly line: number
358
+ /** Column number */
359
+ readonly column: number
360
+ }
361
+
362
+ function createSizeEstimate(
363
+ identifier: string,
364
+ sourceSize: number,
365
+ importCount: number,
366
+ exportCount: number,
367
+ isTransitive: boolean,
368
+ minificationRatio: number,
369
+ gzipRatio: number,
370
+ ): BundleSizeEstimate {
371
+ const estimatedMinifiedSize = Math.floor(sourceSize * minificationRatio)
372
+ const estimatedGzippedSize = Math.floor(estimatedMinifiedSize * gzipRatio)
373
+
374
+ return {
375
+ identifier,
376
+ sourceSize,
377
+ estimatedMinifiedSize,
378
+ estimatedGzippedSize,
379
+ importCount,
380
+ exportCount,
381
+ isTransitive,
382
+ }
383
+ }
384
+
385
+ function collectDependencyEstimates(
386
+ importResults: readonly ImportExtractionResult[],
387
+ _options: Required<BundleEstimatorOptions>,
388
+ ): DependencySizeEstimate[] {
389
+ const depMap = new Map<
390
+ string,
391
+ {estimatedSize?: number; sizeKnown: boolean; locations: string[]}
392
+ >()
393
+
394
+ for (const result of importResults) {
395
+ for (const dep of result.externalDependencies) {
396
+ const baseName = getBasePackageName(dep)
397
+ const existing = depMap.get(baseName)
398
+
399
+ if (existing === undefined) {
400
+ const knownSize = KNOWN_PACKAGE_SIZES[baseName]
401
+ const sizeKnown = knownSize !== undefined
402
+ depMap.set(baseName, {
403
+ estimatedSize: sizeKnown ? knownSize * 1024 : undefined,
404
+ sizeKnown,
405
+ locations: [result.filePath],
406
+ })
407
+ } else {
408
+ existing.locations.push(result.filePath)
409
+ }
410
+ }
411
+ }
412
+
413
+ return Array.from(depMap.entries()).map(([packageName, data]) => ({
414
+ packageName,
415
+ estimatedSize: data.estimatedSize,
416
+ sizeKnown: data.sizeKnown,
417
+ importCount: data.locations.length,
418
+ locations: data.locations,
419
+ }))
420
+ }
421
+
422
+ function getBasePackageName(specifier: string): string {
423
+ if (specifier.startsWith('@')) {
424
+ const parts = specifier.split('/')
425
+ if (parts.length >= 2) {
426
+ return `${parts[0]}/${parts[1]}`
427
+ }
428
+ }
429
+ return specifier.split('/')[0] ?? specifier
430
+ }
431
+
432
+ /**
433
+ * Formats a byte size as a human-readable string.
434
+ */
435
+ export function formatBytes(bytes: number): string {
436
+ if (bytes === 0) return '0 B'
437
+
438
+ const units = ['B', 'KB', 'MB', 'GB']
439
+ const k = 1024
440
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
441
+ const size = bytes / k ** i
442
+
443
+ return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`
444
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Performance analysis utilities for bundle size estimation and optimization detection.
3
+ *
4
+ * Provides tools for identifying performance optimization opportunities including:
5
+ * - Bundle size estimation and large file detection
6
+ * - Tree-shaking blocker identification
7
+ * - Dependency size analysis
8
+ */
9
+
10
+ export {
11
+ estimateDependencySize,
12
+ estimateFileSize,
13
+ estimatePackageBundleSize,
14
+ estimateTreeShakingSavings,
15
+ findLargeDependencies,
16
+ findLargeFiles,
17
+ formatBytes,
18
+ } from './bundle-estimator'
19
+
20
+ export type {
21
+ BundleEstimatorOptions,
22
+ BundleSizeEstimate,
23
+ DependencySizeEstimate,
24
+ OptimizableImport,
25
+ PackageBundleStats,
26
+ TreeShakingSavingsEstimate,
27
+ } from './bundle-estimator'