@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.
- package/README.md +402 -0
- package/lib/chunk-4LSFAAZW.js +1 -0
- package/lib/chunk-JDF7DQ4V.js +27 -0
- package/lib/chunk-WOJ4C7N7.js +7122 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +318 -0
- package/lib/index.d.ts +3701 -0
- package/lib/index.js +1262 -0
- package/lib/types/index.d.ts +146 -0
- package/lib/types/index.js +28 -0
- package/package.json +89 -0
- package/src/analyzers/analyzer.ts +201 -0
- package/src/analyzers/architectural-analyzer.ts +304 -0
- package/src/analyzers/build-config-analyzer.ts +334 -0
- package/src/analyzers/circular-import-analyzer.ts +463 -0
- package/src/analyzers/config-consistency-analyzer.ts +335 -0
- package/src/analyzers/dead-code-analyzer.ts +565 -0
- package/src/analyzers/duplicate-code-analyzer.ts +626 -0
- package/src/analyzers/duplicate-dependency-analyzer.ts +381 -0
- package/src/analyzers/eslint-config-analyzer.ts +281 -0
- package/src/analyzers/exports-field-analyzer.ts +324 -0
- package/src/analyzers/index.ts +388 -0
- package/src/analyzers/large-dependency-analyzer.ts +535 -0
- package/src/analyzers/package-json-analyzer.ts +349 -0
- package/src/analyzers/peer-dependency-analyzer.ts +275 -0
- package/src/analyzers/tree-shaking-analyzer.ts +623 -0
- package/src/analyzers/tsconfig-analyzer.ts +382 -0
- package/src/analyzers/unused-dependency-analyzer.ts +356 -0
- package/src/analyzers/version-alignment-analyzer.ts +308 -0
- package/src/api/analyze-workspace.ts +245 -0
- package/src/api/index.ts +11 -0
- package/src/cache/cache-manager.ts +495 -0
- package/src/cache/cache-schema.ts +247 -0
- package/src/cache/change-detector.ts +169 -0
- package/src/cache/file-hasher.ts +65 -0
- package/src/cache/index.ts +47 -0
- package/src/cli/commands/analyze.ts +240 -0
- package/src/cli/commands/index.ts +5 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/types.ts +65 -0
- package/src/cli/ui.ts +213 -0
- package/src/cli.ts +9 -0
- package/src/config/defaults.ts +183 -0
- package/src/config/index.ts +81 -0
- package/src/config/loader.ts +270 -0
- package/src/config/merger.ts +229 -0
- package/src/config/schema.ts +263 -0
- package/src/core/incremental-analyzer.ts +462 -0
- package/src/core/index.ts +34 -0
- package/src/core/orchestrator.ts +416 -0
- package/src/graph/dependency-graph.ts +408 -0
- package/src/graph/index.ts +19 -0
- package/src/index.ts +417 -0
- package/src/parser/config-parser.ts +491 -0
- package/src/parser/import-extractor.ts +340 -0
- package/src/parser/index.ts +54 -0
- package/src/parser/typescript-parser.ts +95 -0
- package/src/performance/bundle-estimator.ts +444 -0
- package/src/performance/index.ts +27 -0
- package/src/reporters/console-reporter.ts +355 -0
- package/src/reporters/index.ts +49 -0
- package/src/reporters/json-reporter.ts +273 -0
- package/src/reporters/markdown-reporter.ts +349 -0
- package/src/reporters/reporter.ts +399 -0
- package/src/rules/builtin-rules.ts +709 -0
- package/src/rules/index.ts +52 -0
- package/src/rules/rule-engine.ts +409 -0
- package/src/scanner/index.ts +18 -0
- package/src/scanner/workspace-scanner.ts +403 -0
- package/src/types/index.ts +176 -0
- package/src/types/result.ts +19 -0
- package/src/utils/index.ts +7 -0
- 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
|
+
}
|