@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,535 @@
1
+ /**
2
+ * LargeDependencyAnalyzer - Identifies large dependencies that impact bundle size.
3
+ *
4
+ * Analyzes package dependencies to identify:
5
+ * - Known large packages that significantly impact bundle size
6
+ * - Dependencies with lighter alternatives
7
+ * - Packages that may be overkill for the actual usage
8
+ *
9
+ * Uses a database of known package sizes and suggests optimizations.
10
+ */
11
+
12
+ import type {WorkspacePackage} from '../scanner/workspace-scanner'
13
+ import type {Issue, Severity} from '../types/index'
14
+ import type {Result} from '../types/result'
15
+ import type {AnalysisContext, Analyzer, AnalyzerError, AnalyzerMetadata} from './analyzer'
16
+
17
+ import {createProject} from '@bfra.me/doc-sync/parsers'
18
+ import {ok} from '@bfra.me/es/result'
19
+
20
+ import {extractImports, getPackageNameFromSpecifier} from '../parser/import-extractor'
21
+ import {createIssue, filterIssues} from './analyzer'
22
+
23
+ /**
24
+ * Configuration options for LargeDependencyAnalyzer.
25
+ */
26
+ export interface LargeDependencyAnalyzerOptions {
27
+ /** Minimum size threshold in KB (gzipped) to report */
28
+ readonly sizeThresholdKb?: number
29
+ /** Report lighter alternatives when available */
30
+ readonly suggestAlternatives?: boolean
31
+ /** Check for packages with heavy transitive dependencies */
32
+ readonly checkTransitiveDeps?: boolean
33
+ /** Severity for large dependency warnings */
34
+ readonly largeDependencySeverity?: Severity
35
+ /** Severity for alternative suggestions */
36
+ readonly alternativeSuggestionSeverity?: Severity
37
+ /** Packages to ignore */
38
+ readonly ignorePackages?: readonly string[]
39
+ }
40
+
41
+ const DEFAULT_OPTIONS: Required<LargeDependencyAnalyzerOptions> = {
42
+ sizeThresholdKb: 50,
43
+ suggestAlternatives: true,
44
+ checkTransitiveDeps: true,
45
+ largeDependencySeverity: 'info',
46
+ alternativeSuggestionSeverity: 'info',
47
+ ignorePackages: [],
48
+ }
49
+
50
+ export const largeDependencyAnalyzerMetadata: AnalyzerMetadata = {
51
+ id: 'large-dependency',
52
+ name: 'Large Dependency Analyzer',
53
+ description: 'Identifies large dependencies that significantly impact bundle size',
54
+ categories: ['performance'],
55
+ defaultSeverity: 'info',
56
+ }
57
+
58
+ /**
59
+ * Known package sizes and metadata.
60
+ * Sizes are approximate gzipped KB.
61
+ */
62
+ interface PackageInfo {
63
+ /** Approximate gzipped size in KB */
64
+ readonly sizeKb: number
65
+ /** Category of the package */
66
+ readonly category: string
67
+ /** Lighter alternatives (if any) */
68
+ readonly alternatives?: readonly AlternativePackage[]
69
+ /** Whether the package supports tree-shaking */
70
+ readonly treeShakable: boolean
71
+ /** Notes about usage */
72
+ readonly notes?: string
73
+ }
74
+
75
+ interface AlternativePackage {
76
+ /** Package name */
77
+ readonly name: string
78
+ /** Approximate size in KB */
79
+ readonly sizeKb: number
80
+ /** Notes about the alternative */
81
+ readonly notes?: string
82
+ }
83
+
84
+ const KNOWN_PACKAGES: Readonly<Record<string, PackageInfo>> = {
85
+ lodash: {
86
+ sizeKb: 71,
87
+ category: 'utility',
88
+ alternatives: [
89
+ {name: 'lodash-es', sizeKb: 71, notes: 'ES module version with tree-shaking'},
90
+ {name: 'es-toolkit', sizeKb: 10, notes: 'Modern ES alternative with smaller footprint'},
91
+ {name: 'radash', sizeKb: 8, notes: 'TypeScript-first utility library'},
92
+ ],
93
+ treeShakable: false,
94
+ notes: 'CommonJS build prevents tree-shaking. Use lodash-es or individual imports.',
95
+ },
96
+ 'lodash-es': {
97
+ sizeKb: 71,
98
+ category: 'utility',
99
+ alternatives: [{name: 'es-toolkit', sizeKb: 10, notes: 'Modern ES alternative'}],
100
+ treeShakable: true,
101
+ },
102
+ moment: {
103
+ sizeKb: 67,
104
+ category: 'date',
105
+ alternatives: [
106
+ {name: 'dayjs', sizeKb: 3, notes: 'API-compatible, much smaller'},
107
+ {name: 'date-fns', sizeKb: 25, notes: 'Tree-shakable, functional API'},
108
+ {name: 'luxon', sizeKb: 20, notes: 'Modern API, immutable'},
109
+ ],
110
+ treeShakable: false,
111
+ notes: 'Moment is in maintenance mode. Consider migrating to a modern alternative.',
112
+ },
113
+ 'moment-timezone': {
114
+ sizeKb: 95,
115
+ category: 'date',
116
+ alternatives: [
117
+ {
118
+ name: 'dayjs + dayjs/plugin/timezone',
119
+ sizeKb: 6,
120
+ notes: 'Much smaller with timezone support',
121
+ },
122
+ {name: 'date-fns-tz', sizeKb: 5, notes: 'Timezone support for date-fns'},
123
+ ],
124
+ treeShakable: false,
125
+ },
126
+ rxjs: {
127
+ sizeKb: 40,
128
+ category: 'reactive',
129
+ treeShakable: true,
130
+ notes: 'Ensure you use the tree-shakable imports (rxjs/operators)',
131
+ },
132
+ d3: {
133
+ sizeKb: 80,
134
+ category: 'visualization',
135
+ alternatives: [{name: 'd3-* submodules', sizeKb: 5, notes: 'Import only needed D3 modules'}],
136
+ treeShakable: false,
137
+ notes: 'Import specific D3 modules (d3-selection, d3-scale) instead of the full bundle.',
138
+ },
139
+ 'chart.js': {
140
+ sizeKb: 65,
141
+ category: 'visualization',
142
+ alternatives: [
143
+ {name: 'lightweight-charts', sizeKb: 45, notes: 'For financial/trading charts'},
144
+ {name: 'uplot', sizeKb: 8, notes: 'Ultra-lightweight time series charts'},
145
+ ],
146
+ treeShakable: true,
147
+ },
148
+ three: {
149
+ sizeKb: 150,
150
+ category: '3d-graphics',
151
+ treeShakable: true,
152
+ notes: 'Three.js is large but often necessary for 3D. Use code splitting for lazy loading.',
153
+ },
154
+ '@mui/material': {
155
+ sizeKb: 120,
156
+ category: 'ui-framework',
157
+ alternatives: [
158
+ {name: '@radix-ui/*', sizeKb: 30, notes: 'Headless UI primitives, smaller footprint'},
159
+ {name: '@headlessui/react', sizeKb: 10, notes: 'Unstyled, accessible components'},
160
+ ],
161
+ treeShakable: true,
162
+ notes: 'Ensure proper tree-shaking by using named imports.',
163
+ },
164
+ antd: {
165
+ sizeKb: 200,
166
+ category: 'ui-framework',
167
+ alternatives: [
168
+ {name: '@arco-design/web-react', sizeKb: 100, notes: 'Similar component library, smaller'},
169
+ ],
170
+ treeShakable: true,
171
+ notes: 'Very large. Consider using code splitting and lazy loading components.',
172
+ },
173
+ axios: {
174
+ sizeKb: 13,
175
+ category: 'http',
176
+ alternatives: [
177
+ {name: 'ky', sizeKb: 3, notes: 'Smaller fetch wrapper'},
178
+ {name: 'native fetch', sizeKb: 0, notes: 'Built into modern browsers and Node.js 18+'},
179
+ ],
180
+ treeShakable: false,
181
+ },
182
+ underscore: {
183
+ sizeKb: 18,
184
+ category: 'utility',
185
+ alternatives: [
186
+ {name: 'lodash-es', sizeKb: 71, notes: 'More features but tree-shakable'},
187
+ {name: 'es-toolkit', sizeKb: 10, notes: 'Modern TypeScript utilities'},
188
+ ],
189
+ treeShakable: false,
190
+ },
191
+ jquery: {
192
+ sizeKb: 30,
193
+ category: 'dom',
194
+ alternatives: [
195
+ {name: 'native DOM APIs', sizeKb: 0, notes: 'Modern browsers have good DOM APIs'},
196
+ ],
197
+ treeShakable: false,
198
+ notes: 'jQuery is rarely needed in modern applications with frameworks.',
199
+ },
200
+ 'monaco-editor': {
201
+ sizeKb: 2000,
202
+ category: 'editor',
203
+ treeShakable: false,
204
+ notes: 'Very large (~2MB). Use dynamic imports and load only needed language workers.',
205
+ },
206
+ 'highlight.js': {
207
+ sizeKb: 100,
208
+ category: 'syntax-highlighting',
209
+ alternatives: [
210
+ {name: 'prismjs', sizeKb: 10, notes: 'Smaller with selective language loading'},
211
+ {name: 'shiki', sizeKb: 30, notes: 'VS Code highlighting engine'},
212
+ ],
213
+ treeShakable: false,
214
+ notes: 'Import only needed languages to reduce size.',
215
+ },
216
+ typescript: {
217
+ sizeKb: 150,
218
+ category: 'compiler',
219
+ treeShakable: false,
220
+ notes: 'Usually a devDependency. Should not be in production bundles.',
221
+ },
222
+ 'ts-morph': {
223
+ sizeKb: 150,
224
+ category: 'ast',
225
+ treeShakable: false,
226
+ notes: 'Usually for build tools. Should not be in production bundles.',
227
+ },
228
+ typeorm: {
229
+ sizeKb: 180,
230
+ category: 'orm',
231
+ alternatives: [
232
+ {name: 'drizzle-orm', sizeKb: 30, notes: 'Lightweight TypeScript ORM'},
233
+ {name: 'prisma', sizeKb: 40, notes: 'Type-safe database client'},
234
+ ],
235
+ treeShakable: false,
236
+ },
237
+ yup: {
238
+ sizeKb: 22,
239
+ category: 'validation',
240
+ alternatives: [
241
+ {name: 'zod', sizeKb: 12, notes: 'TypeScript-first validation, smaller'},
242
+ {name: 'valibot', sizeKb: 3, notes: 'Modular validation library'},
243
+ ],
244
+ treeShakable: false,
245
+ },
246
+ joi: {
247
+ sizeKb: 35,
248
+ category: 'validation',
249
+ alternatives: [
250
+ {name: 'zod', sizeKb: 12, notes: 'TypeScript-first validation'},
251
+ {name: 'valibot', sizeKb: 3, notes: 'Modular validation library'},
252
+ ],
253
+ treeShakable: false,
254
+ },
255
+ validator: {
256
+ sizeKb: 25,
257
+ category: 'validation',
258
+ alternatives: [{name: 'is-* packages', sizeKb: 1, notes: 'Individual validation functions'}],
259
+ treeShakable: false,
260
+ },
261
+ 'pdf-lib': {
262
+ sizeKb: 300,
263
+ category: 'pdf',
264
+ treeShakable: false,
265
+ notes: 'Large but feature-rich. Use dynamic imports for PDF generation features.',
266
+ },
267
+ xlsx: {
268
+ sizeKb: 200,
269
+ category: 'spreadsheet',
270
+ treeShakable: false,
271
+ notes: 'Very large. Consider server-side processing or dynamic imports.',
272
+ },
273
+ jszip: {
274
+ sizeKb: 45,
275
+ category: 'compression',
276
+ treeShakable: false,
277
+ },
278
+ 'core-js': {
279
+ sizeKb: 150,
280
+ category: 'polyfill',
281
+ treeShakable: true,
282
+ notes: 'Often unnecessary with modern browser targets. Check your browserslist.',
283
+ },
284
+ 'babel-polyfill': {
285
+ sizeKb: 100,
286
+ category: 'polyfill',
287
+ alternatives: [{name: 'core-js/stable', sizeKb: 50, notes: 'Selective polyfills'}],
288
+ treeShakable: false,
289
+ notes: 'Deprecated. Use core-js directly with selective imports.',
290
+ },
291
+ }
292
+
293
+ /**
294
+ * Creates a LargeDependencyAnalyzer instance.
295
+ */
296
+ export function createLargeDependencyAnalyzer(
297
+ options: LargeDependencyAnalyzerOptions = {},
298
+ ): Analyzer {
299
+ const resolvedOptions = {...DEFAULT_OPTIONS, ...options}
300
+
301
+ return {
302
+ metadata: largeDependencyAnalyzerMetadata,
303
+ analyze: async (context: AnalysisContext): Promise<Result<readonly Issue[], AnalyzerError>> => {
304
+ const issues: Issue[] = []
305
+
306
+ for (const pkg of context.packages) {
307
+ context.reportProgress?.(`Analyzing dependencies in ${pkg.name}...`)
308
+
309
+ const packageIssues = await analyzePackageDependencies(pkg, resolvedOptions)
310
+ issues.push(...packageIssues)
311
+ }
312
+
313
+ return ok(filterIssues(issues, context.config))
314
+ },
315
+ }
316
+ }
317
+
318
+ async function analyzePackageDependencies(
319
+ pkg: WorkspacePackage,
320
+ options: Required<LargeDependencyAnalyzerOptions>,
321
+ ): Promise<Issue[]> {
322
+ const issues: Issue[] = []
323
+
324
+ // Collect all dependencies
325
+ const allDeps = collectAllDependencies(pkg)
326
+
327
+ // Check each dependency against known large packages
328
+ for (const [depName, depVersion] of allDeps) {
329
+ if (options.ignorePackages.includes(depName)) continue
330
+
331
+ const info = KNOWN_PACKAGES[depName]
332
+ if (info === undefined) continue
333
+
334
+ if (info.sizeKb < options.sizeThresholdKb) continue
335
+
336
+ // Create issue for large dependency
337
+ issues.push(createLargeDependencyIssue(pkg, depName, depVersion, info, options))
338
+
339
+ // Suggest alternatives if available and configured
340
+ if (
341
+ options.suggestAlternatives &&
342
+ info.alternatives !== undefined &&
343
+ info.alternatives.length > 0
344
+ ) {
345
+ issues.push(createAlternativeSuggestionIssue(pkg, depName, info, options))
346
+ }
347
+ }
348
+
349
+ // Check for problematic patterns in how dependencies are used
350
+ const usageIssues = await analyzeDependencyUsage(pkg, allDeps, options)
351
+ issues.push(...usageIssues)
352
+
353
+ return issues
354
+ }
355
+
356
+ function collectAllDependencies(pkg: WorkspacePackage): Map<string, string> {
357
+ const deps = new Map<string, string>()
358
+ const pkgJson = pkg.packageJson
359
+
360
+ if (pkgJson.dependencies !== undefined) {
361
+ for (const [name, version] of Object.entries(pkgJson.dependencies)) {
362
+ deps.set(name, version)
363
+ }
364
+ }
365
+
366
+ // Also check devDependencies for build tools that shouldn't be bundled
367
+ if (pkgJson.devDependencies !== undefined) {
368
+ for (const [name, version] of Object.entries(pkgJson.devDependencies)) {
369
+ const info = KNOWN_PACKAGES[name]
370
+ if (info?.category === 'compiler' || info?.category === 'ast') {
371
+ deps.set(name, version)
372
+ }
373
+ }
374
+ }
375
+
376
+ return deps
377
+ }
378
+
379
+ async function analyzeDependencyUsage(
380
+ pkg: WorkspacePackage,
381
+ allDeps: Map<string, string>,
382
+ options: Required<LargeDependencyAnalyzerOptions>,
383
+ ): Promise<Issue[]> {
384
+ const issues: Issue[] = []
385
+ const project = createProject()
386
+
387
+ // Analyze usage patterns for specific problematic imports
388
+ const importCounts = new Map<string, {count: number; locations: string[]}>()
389
+
390
+ for (const filePath of pkg.sourceFiles) {
391
+ try {
392
+ const sourceFile = project.addSourceFileAtPath(filePath)
393
+ const result = extractImports(sourceFile)
394
+
395
+ for (const imp of result.imports) {
396
+ const baseName = getPackageNameFromSpecifier(imp.moduleSpecifier)
397
+ if (!allDeps.has(baseName)) continue
398
+
399
+ const existing = importCounts.get(baseName)
400
+ if (existing === undefined) {
401
+ importCounts.set(baseName, {count: 1, locations: [filePath]})
402
+ } else {
403
+ existing.count++
404
+ if (!existing.locations.includes(filePath)) {
405
+ existing.locations.push(filePath)
406
+ }
407
+ }
408
+
409
+ // Check for problematic import patterns
410
+ const info = KNOWN_PACKAGES[baseName]
411
+ if (info === undefined) continue
412
+
413
+ // Check for full package import of non-tree-shakable packages
414
+ if (!info.treeShakable && imp.namespaceImport !== undefined) {
415
+ issues.push(
416
+ createIssue({
417
+ id: 'non-treeshakable-namespace-import',
418
+ title: `Namespace import of non-tree-shakable package '${baseName}'`,
419
+ description:
420
+ `'${baseName}' (${info.sizeKb}KB) doesn't support tree-shaking and is imported with a namespace ` +
421
+ `import, resulting in the entire package being bundled.`,
422
+ severity: options.largeDependencySeverity,
423
+ category: 'performance',
424
+ location: {filePath, line: imp.line, column: imp.column},
425
+ suggestion:
426
+ info.notes ??
427
+ `Consider using named imports for only the functions you need, or switch to a tree-shakable alternative.`,
428
+ metadata: {
429
+ packageName: pkg.name,
430
+ dependency: baseName,
431
+ sizeKb: info.sizeKb,
432
+ importPattern: 'namespace',
433
+ },
434
+ }),
435
+ )
436
+ }
437
+ }
438
+ } catch {
439
+ // File may not be parseable
440
+ }
441
+ }
442
+
443
+ return issues
444
+ }
445
+
446
+ function createLargeDependencyIssue(
447
+ pkg: WorkspacePackage,
448
+ depName: string,
449
+ depVersion: string,
450
+ info: PackageInfo,
451
+ options: Required<LargeDependencyAnalyzerOptions>,
452
+ ): Issue {
453
+ const pkgJsonPath = `${pkg.packagePath}/package.json`
454
+
455
+ return createIssue({
456
+ id: 'large-dependency',
457
+ title: `Large dependency: '${depName}' (~${info.sizeKb}KB gzipped)`,
458
+ description:
459
+ `'${depName}@${depVersion}' adds approximately ${info.sizeKb}KB to your bundle (gzipped). ` +
460
+ `Category: ${info.category}. ${info.treeShakable ? 'Supports tree-shaking.' : 'Does NOT support tree-shaking.'}`,
461
+ severity: options.largeDependencySeverity,
462
+ category: 'performance',
463
+ location: {filePath: pkgJsonPath},
464
+ suggestion: info.notes ?? `Review if all features of '${depName}' are needed.`,
465
+ metadata: {
466
+ packageName: pkg.name,
467
+ dependency: depName,
468
+ version: depVersion,
469
+ sizeKb: info.sizeKb,
470
+ category: info.category,
471
+ treeShakable: info.treeShakable,
472
+ hasAlternatives: info.alternatives !== undefined && info.alternatives.length > 0,
473
+ },
474
+ })
475
+ }
476
+
477
+ function createAlternativeSuggestionIssue(
478
+ pkg: WorkspacePackage,
479
+ depName: string,
480
+ info: PackageInfo,
481
+ options: Required<LargeDependencyAnalyzerOptions>,
482
+ ): Issue {
483
+ const pkgJsonPath = `${pkg.packagePath}/package.json`
484
+ const alternatives = info.alternatives ?? []
485
+
486
+ const alternativeList = alternatives
487
+ .map(alt => {
488
+ const notes = alt.notes
489
+ return notes === undefined
490
+ ? `• ${alt.name} (~${alt.sizeKb}KB)`
491
+ : `• ${alt.name} (~${alt.sizeKb}KB): ${notes}`
492
+ })
493
+ .join('\n')
494
+
495
+ const firstAlt = alternatives[0]
496
+ const bestAlt =
497
+ firstAlt === undefined
498
+ ? undefined
499
+ : alternatives.reduce((best, alt) => (alt.sizeKb < best.sizeKb ? alt : best), firstAlt)
500
+
501
+ const potentialSavings = bestAlt === undefined ? 0 : info.sizeKb - bestAlt.sizeKb
502
+
503
+ return createIssue({
504
+ id: 'lighter-alternative-available',
505
+ title: `Lighter alternatives available for '${depName}'`,
506
+ description: `'${depName}' (~${info.sizeKb}KB) has lighter alternatives that could save up to ~${potentialSavings}KB:\n${alternativeList}`,
507
+ severity: options.alternativeSuggestionSeverity,
508
+ category: 'performance',
509
+ location: {filePath: pkgJsonPath},
510
+ suggestion:
511
+ `Consider switching to a lighter alternative if your use case is supported. ` +
512
+ `Potential bundle size savings: ~${potentialSavings}KB gzipped.`,
513
+ metadata: {
514
+ packageName: pkg.name,
515
+ dependency: depName,
516
+ currentSizeKb: info.sizeKb,
517
+ alternatives: alternatives.map(alt => ({name: alt.name, sizeKb: alt.sizeKb})),
518
+ potentialSavingsKb: potentialSavings,
519
+ },
520
+ })
521
+ }
522
+
523
+ /**
524
+ * Gets known package information for a dependency.
525
+ */
526
+ export function getPackageInfo(packageName: string): PackageInfo | undefined {
527
+ return KNOWN_PACKAGES[packageName]
528
+ }
529
+
530
+ /**
531
+ * Gets all known large packages.
532
+ */
533
+ export function getKnownLargePackages(): readonly string[] {
534
+ return Object.keys(KNOWN_PACKAGES)
535
+ }