whyinstall 0.2.0 → 0.3.1
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 +33 -0
- package/dist/analyzer.d.ts +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +2 -8
- package/dist/analyzer.js.map +1 -1
- package/dist/cli.js +13 -5
- package/dist/cli.js.map +1 -1
- package/dist/fileFinder.d.ts +0 -8
- package/dist/fileFinder.d.ts.map +1 -1
- package/dist/fileFinder.js +0 -94
- package/dist/fileFinder.js.map +1 -1
- package/dist/formatter.d.ts +3 -2
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +19 -49
- package/dist/formatter.js.map +1 -1
- package/dist/sizeMapAnalyzer.d.ts +3 -0
- package/dist/sizeMapAnalyzer.d.ts.map +1 -0
- package/dist/sizeMapAnalyzer.js +134 -0
- package/dist/sizeMapAnalyzer.js.map +1 -0
- package/dist/types.d.ts +11 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/analyzer.ts +2 -9
- package/src/cli.ts +14 -7
- package/src/fileFinder.ts +0 -111
- package/src/formatter.ts +24 -57
- package/src/sizeMapAnalyzer.ts +145 -0
- package/src/types.ts +13 -15
- package/src/impactAnalyzer.ts +0 -139
package/dist/types.d.ts
CHANGED
|
@@ -3,18 +3,6 @@ export interface DependencyPath {
|
|
|
3
3
|
type: 'prod' | 'dev' | 'peer' | 'optional';
|
|
4
4
|
packageJsonPath?: string;
|
|
5
5
|
}
|
|
6
|
-
export interface FileUsage {
|
|
7
|
-
file: string;
|
|
8
|
-
lines: number[];
|
|
9
|
-
methods: string[];
|
|
10
|
-
context?: string[];
|
|
11
|
-
purpose?: string;
|
|
12
|
-
}
|
|
13
|
-
export interface ImpactAnalysis {
|
|
14
|
-
files: FileUsage[];
|
|
15
|
-
riskLevel: 'Low' | 'Medium' | 'High';
|
|
16
|
-
impacts: string[];
|
|
17
|
-
}
|
|
18
6
|
export interface PackageInfo {
|
|
19
7
|
name: string;
|
|
20
8
|
version: string;
|
|
@@ -22,11 +10,21 @@ export interface PackageInfo {
|
|
|
22
10
|
size?: number;
|
|
23
11
|
paths: DependencyPath[];
|
|
24
12
|
sourceFiles?: string[];
|
|
25
|
-
impact?: ImpactAnalysis;
|
|
26
13
|
}
|
|
27
14
|
export interface AnalyzeResult {
|
|
28
15
|
package: PackageInfo;
|
|
29
16
|
suggestions: string[];
|
|
30
17
|
}
|
|
31
18
|
export type PackageManager = 'npm' | 'yarn' | 'pnpm';
|
|
19
|
+
export interface SizeBreakdown {
|
|
20
|
+
name: string;
|
|
21
|
+
size: number;
|
|
22
|
+
}
|
|
23
|
+
export interface SizeMapResult {
|
|
24
|
+
packageName: string;
|
|
25
|
+
totalSize: number;
|
|
26
|
+
breakdown: SizeBreakdown[];
|
|
27
|
+
nodeModulesSize?: number;
|
|
28
|
+
percentOfNodeModules?: number;
|
|
29
|
+
}
|
|
32
30
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,UAAU,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,UAAU,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAErD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B"}
|
package/package.json
CHANGED
package/src/analyzer.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { join, dirname } from 'path';
|
|
|
3
3
|
import { PackageInfo, DependencyPath, AnalyzeResult } from './types';
|
|
4
4
|
import { detectPackageManager } from './packageManager';
|
|
5
5
|
import { findFilesUsingPackage } from './fileFinder';
|
|
6
|
-
import { analyzeImpact } from './impactAnalyzer';
|
|
7
6
|
|
|
8
7
|
interface PackageJson {
|
|
9
8
|
name?: string;
|
|
@@ -192,7 +191,7 @@ function formatSize(bytes: number | undefined): string {
|
|
|
192
191
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
193
192
|
}
|
|
194
193
|
|
|
195
|
-
export function analyzePackage(packageName: string, cwd: string = process.cwd()
|
|
194
|
+
export function analyzePackage(packageName: string, cwd: string = process.cwd()): AnalyzeResult {
|
|
196
195
|
const rootPackageJson = join(cwd, 'package.json');
|
|
197
196
|
const packageJsonPath = findPackageJsonPath(packageName, cwd);
|
|
198
197
|
if (!packageJsonPath) {
|
|
@@ -242,11 +241,6 @@ export function analyzePackage(packageName: string, cwd: string = process.cwd(),
|
|
|
242
241
|
|
|
243
242
|
const sourceFiles = findFilesUsingPackage(packageName, cwd);
|
|
244
243
|
|
|
245
|
-
let impact;
|
|
246
|
-
if (includeImpact) {
|
|
247
|
-
impact = analyzeImpact(packageName, cwd);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
244
|
return {
|
|
251
245
|
package: {
|
|
252
246
|
name: packageName,
|
|
@@ -254,8 +248,7 @@ export function analyzePackage(packageName: string, cwd: string = process.cwd(),
|
|
|
254
248
|
description,
|
|
255
249
|
size,
|
|
256
250
|
paths,
|
|
257
|
-
sourceFiles: sourceFiles.length > 0 ? sourceFiles : undefined
|
|
258
|
-
impact
|
|
251
|
+
sourceFiles: sourceFiles.length > 0 ? sourceFiles : undefined
|
|
259
252
|
},
|
|
260
253
|
suggestions
|
|
261
254
|
};
|
package/src/cli.ts
CHANGED
|
@@ -3,20 +3,21 @@
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { analyzePackage } from './analyzer';
|
|
6
|
-
import { formatOutput } from './formatter';
|
|
6
|
+
import { formatOutput, formatSizeMap } from './formatter';
|
|
7
7
|
import { detectPackageManager } from './packageManager';
|
|
8
|
+
import { analyzeSizeMap } from './sizeMapAnalyzer';
|
|
8
9
|
|
|
9
10
|
const program = new Command();
|
|
10
11
|
|
|
11
12
|
program
|
|
12
13
|
.name('whyinstall')
|
|
13
14
|
.description('Find why a dependency exists in your JS/TS project')
|
|
14
|
-
.version('0.
|
|
15
|
+
.version('0.3.1')
|
|
15
16
|
.argument('<package-name>', 'Package name to analyze')
|
|
16
17
|
.option('-j, --json', 'Output as JSON')
|
|
17
18
|
.option('-c, --cwd <path>', 'Working directory', process.cwd())
|
|
18
|
-
.option('--
|
|
19
|
-
.action((packageName: string, options: { json?: boolean; cwd?: string;
|
|
19
|
+
.option('-s, --size-map', 'Show bundle size impact breakdown')
|
|
20
|
+
.action((packageName: string, options: { json?: boolean; cwd?: string; sizeMap?: boolean }) => {
|
|
20
21
|
try {
|
|
21
22
|
const cwd = options.cwd || process.cwd();
|
|
22
23
|
const pm = detectPackageManager(cwd);
|
|
@@ -25,9 +26,15 @@ program
|
|
|
25
26
|
console.log(`\n${chalk.gray(`Detected package manager: ${pm}`)}\n`);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
if (options.sizeMap) {
|
|
30
|
+
const result = analyzeSizeMap(packageName, cwd);
|
|
31
|
+
const output = formatSizeMap(result, options.json);
|
|
32
|
+
console.log(output);
|
|
33
|
+
} else {
|
|
34
|
+
const result = analyzePackage(packageName, cwd);
|
|
35
|
+
const output = formatOutput(result, options.json);
|
|
36
|
+
console.log(output);
|
|
37
|
+
}
|
|
31
38
|
|
|
32
39
|
process.exit(0);
|
|
33
40
|
} catch (error) {
|
package/src/fileFinder.ts
CHANGED
|
@@ -46,14 +46,6 @@ export function findSourceFiles(dir: string, maxDepth: number = 5, currentDepth:
|
|
|
46
46
|
return files;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export interface FileUsage {
|
|
50
|
-
file: string;
|
|
51
|
-
lines: number[];
|
|
52
|
-
methods: string[];
|
|
53
|
-
context?: string[];
|
|
54
|
-
purpose?: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
49
|
function fileContainsPackage(filePath: string, packageName: string): boolean {
|
|
58
50
|
try {
|
|
59
51
|
const content = readFileSync(filePath, 'utf-8');
|
|
@@ -71,109 +63,6 @@ function fileContainsPackage(filePath: string, packageName: string): boolean {
|
|
|
71
63
|
}
|
|
72
64
|
}
|
|
73
65
|
|
|
74
|
-
export function analyzeFileUsage(filePath: string, packageName: string, cwd: string): FileUsage | null {
|
|
75
|
-
try {
|
|
76
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
77
|
-
const lines = content.split('\n');
|
|
78
|
-
const relativePath = filePath.replace(cwd + '/', '');
|
|
79
|
-
|
|
80
|
-
const importPatterns = [
|
|
81
|
-
new RegExp(`require\\(['"]${packageName}(/.*)?['"]\\)`, 'g'),
|
|
82
|
-
new RegExp(`from\\s+['"]${packageName}(/.*)?['"]`, 'g'),
|
|
83
|
-
new RegExp(`import\\s+.*\\s+from\\s+['"]${packageName}(/.*)?['"]`, 'g'),
|
|
84
|
-
new RegExp(`import\\s+['"]${packageName}(/.*)?['"]`, 'g'),
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
const usageLines: number[] = [];
|
|
88
|
-
const methods = new Set<string>();
|
|
89
|
-
const context: string[] = [];
|
|
90
|
-
|
|
91
|
-
// Find import lines
|
|
92
|
-
lines.forEach((line, index) => {
|
|
93
|
-
const lineNum = index + 1;
|
|
94
|
-
if (importPatterns.some(pattern => pattern.test(line))) {
|
|
95
|
-
usageLines.push(lineNum);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Find method usage patterns:
|
|
100
|
-
const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
101
|
-
|
|
102
|
-
// 1. Direct: packageName.method() or packageName.method
|
|
103
|
-
const directMethodPattern = new RegExp('\\b' + escapedPackageName + '\\.(\\w+)', 'g');
|
|
104
|
-
const directMatches = content.matchAll(directMethodPattern);
|
|
105
|
-
for (const match of directMatches) {
|
|
106
|
-
methods.add(match[1]);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 2. Destructured imports: const { red, bold } = require('chalk')
|
|
110
|
-
const destructurePattern = new RegExp('(?:const|let|var)\\s*\\{[^}]*\\}\\s*=\\s*(?:require|import)\\s*\\(?[\'"]' + escapedPackageName, 'g');
|
|
111
|
-
if (destructurePattern.test(content)) {
|
|
112
|
-
const destructureMatch = content.match(/(?:const|let|var)\s*\{([^}]+)\}/);
|
|
113
|
-
if (destructureMatch) {
|
|
114
|
-
destructureMatch[1].split(',').forEach(m => {
|
|
115
|
-
const method = m.trim().split(':')[0].trim();
|
|
116
|
-
if (method) methods.add(method);
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 3. Default import assigned to variable: const program = new Command(); program.method()
|
|
122
|
-
// Find named imports: import { Command } from 'commander'
|
|
123
|
-
const namedImportMatch = content.match(/import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/i);
|
|
124
|
-
if (namedImportMatch && namedImportMatch[2] === packageName) {
|
|
125
|
-
const namedImports = namedImportMatch[1].split(',').map(i => i.trim().split('as')[0].trim());
|
|
126
|
-
namedImports.forEach(importName => {
|
|
127
|
-
// Find instances: const program = new Command()
|
|
128
|
-
const escapedImportName = importName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
129
|
-
const instancePattern = new RegExp('(?:const|let|var)\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*=\\s*new\\s+' + escapedImportName, 'g');
|
|
130
|
-
const instanceMatches = content.matchAll(instancePattern);
|
|
131
|
-
for (const instanceMatch of instanceMatches) {
|
|
132
|
-
const instanceName = instanceMatch[1];
|
|
133
|
-
// Find methods on this instance: program.command()
|
|
134
|
-
const instanceMethodPattern = new RegExp('\\b' + instanceName + '\\.(\\w+)\\s*\\(', 'g');
|
|
135
|
-
const methodMatches = content.matchAll(instanceMethodPattern);
|
|
136
|
-
for (const methodMatch of methodMatches) {
|
|
137
|
-
methods.add(methodMatch[1]);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 4. Default import: import Command from 'commander' or const Command = require('commander')
|
|
144
|
-
const defaultImportMatch = content.match(new RegExp('(?:import|const|let|var)\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*(?:=|from)\\s*[\'"]' + escapedPackageName, 'i'));
|
|
145
|
-
if (defaultImportMatch) {
|
|
146
|
-
const importedName = defaultImportMatch[1];
|
|
147
|
-
// Find all method calls on this imported name: program.method()
|
|
148
|
-
const instanceMethodPattern = new RegExp('\\b' + importedName + '\\.(\\w+)\\s*\\(', 'g');
|
|
149
|
-
const instanceMatches = content.matchAll(instanceMethodPattern);
|
|
150
|
-
for (const match of instanceMatches) {
|
|
151
|
-
methods.add(match[1]);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Capture context (lines around usage)
|
|
156
|
-
if (usageLines.length > 0) {
|
|
157
|
-
const firstLine = Math.max(0, usageLines[0] - 3);
|
|
158
|
-
const lastLine = Math.min(lines.length, usageLines[usageLines.length - 1] + 3);
|
|
159
|
-
context.push(...lines.slice(firstLine, lastLine));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (usageLines.length > 0) {
|
|
163
|
-
return {
|
|
164
|
-
file: relativePath,
|
|
165
|
-
lines: usageLines,
|
|
166
|
-
methods: Array.from(methods),
|
|
167
|
-
context: context.slice(0, 10) // Limit context
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return null;
|
|
172
|
-
} catch {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
66
|
export function findFilesUsingPackage(packageName: string, cwd: string): string[] {
|
|
178
67
|
const sourceFiles = findSourceFiles(cwd);
|
|
179
68
|
const matchingFiles: string[] = [];
|
package/src/formatter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { AnalyzeResult, DependencyPath } from './types';
|
|
2
|
+
import { AnalyzeResult, DependencyPath, SizeMapResult } from './types';
|
|
3
3
|
|
|
4
4
|
function formatSize(bytes: number | undefined): string {
|
|
5
5
|
if (!bytes) return '';
|
|
@@ -55,25 +55,7 @@ function getTypeLabel(type: DependencyPath['type']): string {
|
|
|
55
55
|
return labels[type] || type;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function
|
|
59
|
-
const emojis = {
|
|
60
|
-
Low: '🟢',
|
|
61
|
-
Medium: '🟡',
|
|
62
|
-
High: '🔴'
|
|
63
|
-
};
|
|
64
|
-
return emojis[risk] || '⚪';
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getRiskColor(risk: 'Low' | 'Medium' | 'High'): (text: string) => string {
|
|
68
|
-
const colors = {
|
|
69
|
-
Low: chalk.green,
|
|
70
|
-
Medium: chalk.yellow,
|
|
71
|
-
High: chalk.red
|
|
72
|
-
};
|
|
73
|
-
return colors[risk] || chalk.gray;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function formatOutput(result: AnalyzeResult, json: boolean = false, showImpact: boolean = false): string {
|
|
58
|
+
export function formatOutput(result: AnalyzeResult, json: boolean = false): string {
|
|
77
59
|
if (json) {
|
|
78
60
|
return JSON.stringify(result, null, 2);
|
|
79
61
|
}
|
|
@@ -111,43 +93,6 @@ export function formatOutput(result: AnalyzeResult, json: boolean = false, showI
|
|
|
111
93
|
});
|
|
112
94
|
}
|
|
113
95
|
|
|
114
|
-
if (showImpact && pkg.impact) {
|
|
115
|
-
const impact = pkg.impact;
|
|
116
|
-
output += '\n' + chalk.bold('Impact analysis:') + '\n\n';
|
|
117
|
-
|
|
118
|
-
if (impact.files.length > 0) {
|
|
119
|
-
output += chalk.bold('Used in ') + chalk.bold(`${impact.files.length} file${impact.files.length !== 1 ? 's' : ''}:`) + '\n\n';
|
|
120
|
-
|
|
121
|
-
impact.files.forEach((file, index) => {
|
|
122
|
-
const fileUsage = file as any;
|
|
123
|
-
const lineRange = fileUsage.lines.length === 1
|
|
124
|
-
? `Line ${fileUsage.lines[0]}`
|
|
125
|
-
: `Lines ${Math.min(...fileUsage.lines)}–${Math.max(...fileUsage.lines)}`;
|
|
126
|
-
|
|
127
|
-
output += `${index + 1}. ${chalk.blue(fileUsage.file)}\n`;
|
|
128
|
-
output += ` ${chalk.gray(lineRange)}\n`;
|
|
129
|
-
if (fileUsage.purpose) {
|
|
130
|
-
output += ` ${chalk.dim(`Purpose: ${fileUsage.purpose}`)}\n`;
|
|
131
|
-
}
|
|
132
|
-
if (fileUsage.methods.length > 0) {
|
|
133
|
-
output += ` ${chalk.dim(`Methods: ${fileUsage.methods.slice(0, 5).join(', ')}${fileUsage.methods.length > 5 ? '...' : ''}`)}\n`;
|
|
134
|
-
}
|
|
135
|
-
output += '\n';
|
|
136
|
-
});
|
|
137
|
-
} else {
|
|
138
|
-
output += chalk.yellow(' No direct usage found in source files\n\n');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
output += chalk.bold('Estimated removal impact:') + '\n';
|
|
142
|
-
impact.impacts.forEach((impactText) => {
|
|
143
|
-
output += ` ${chalk.gray('-')} ${chalk.gray(impactText)}\n`;
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
output += '\n';
|
|
147
|
-
const riskColor = getRiskColor(impact.riskLevel);
|
|
148
|
-
output += chalk.bold('Risk level: ') + getRiskEmoji(impact.riskLevel) + ' ' + riskColor(impact.riskLevel) + '\n';
|
|
149
|
-
}
|
|
150
|
-
|
|
151
96
|
if (suggestions.length > 0) {
|
|
152
97
|
output += '\n' + chalk.bold('Suggested actions:') + '\n';
|
|
153
98
|
suggestions.forEach((suggestion, index) => {
|
|
@@ -157,3 +102,25 @@ export function formatOutput(result: AnalyzeResult, json: boolean = false, showI
|
|
|
157
102
|
|
|
158
103
|
return output;
|
|
159
104
|
}
|
|
105
|
+
|
|
106
|
+
export function formatSizeMap(result: SizeMapResult, json: boolean = false): string {
|
|
107
|
+
if (json) {
|
|
108
|
+
return JSON.stringify(result, null, 2);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let output = '';
|
|
112
|
+
output += chalk.bold.cyan(`Size map for: ${result.packageName}\n\n`);
|
|
113
|
+
output += chalk.bold(`${result.packageName} total impact: `) + chalk.green(formatSize(result.totalSize)) + '\n\n';
|
|
114
|
+
output += chalk.bold('Breakdown:\n');
|
|
115
|
+
|
|
116
|
+
for (const item of result.breakdown) {
|
|
117
|
+
const sizeStr = formatSize(item.size);
|
|
118
|
+
output += chalk.gray('- ') + chalk.white(item.name) + chalk.gray(': ') + chalk.yellow(sizeStr) + '\n';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (result.percentOfNodeModules !== undefined && result.percentOfNodeModules > 0) {
|
|
122
|
+
output += '\n' + chalk.dim(`This package contributes ${result.percentOfNodeModules.toFixed(1)}% of your vendor bundle.\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return output;
|
|
126
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, dirname, extname } from 'path';
|
|
3
|
+
import { SizeMapResult, SizeBreakdown } from './types';
|
|
4
|
+
|
|
5
|
+
interface PackageJson {
|
|
6
|
+
name?: string;
|
|
7
|
+
dependencies?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const JS_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
|
|
11
|
+
const EXCLUDE_DIRS = new Set(['test', 'tests', '__tests__', 'docs', 'doc', 'types', '@types', 'typings', '.d.ts']);
|
|
12
|
+
|
|
13
|
+
function readPackageJson(path: string): PackageJson | null {
|
|
14
|
+
try {
|
|
15
|
+
if (existsSync(path)) {
|
|
16
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
17
|
+
}
|
|
18
|
+
} catch {}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function shouldExclude(name: string): boolean {
|
|
23
|
+
const lower = name.toLowerCase();
|
|
24
|
+
return EXCLUDE_DIRS.has(lower) || lower.endsWith('.d.ts') || lower.startsWith('.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function calculateJsSize(dir: string): number {
|
|
28
|
+
let total = 0;
|
|
29
|
+
try {
|
|
30
|
+
const entries = readdirSync(dir);
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (shouldExclude(entry)) continue;
|
|
33
|
+
const fullPath = join(dir, entry);
|
|
34
|
+
try {
|
|
35
|
+
const stats = statSync(fullPath);
|
|
36
|
+
if (stats.isDirectory()) {
|
|
37
|
+
if (entry !== 'node_modules') {
|
|
38
|
+
total += calculateJsSize(fullPath);
|
|
39
|
+
}
|
|
40
|
+
} else if (JS_EXTENSIONS.has(extname(entry).toLowerCase())) {
|
|
41
|
+
total += stats.size;
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
return total;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getNodeModulesSize(cwd: string): number {
|
|
50
|
+
const nmPath = join(cwd, 'node_modules');
|
|
51
|
+
let total = 0;
|
|
52
|
+
|
|
53
|
+
function walkDir(dir: string): void {
|
|
54
|
+
try {
|
|
55
|
+
const entries = readdirSync(dir);
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const fullPath = join(dir, entry);
|
|
58
|
+
try {
|
|
59
|
+
const stats = statSync(fullPath);
|
|
60
|
+
if (stats.isDirectory()) {
|
|
61
|
+
walkDir(fullPath);
|
|
62
|
+
} else if (JS_EXTENSIONS.has(extname(entry).toLowerCase())) {
|
|
63
|
+
total += stats.size;
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
walkDir(nmPath);
|
|
71
|
+
return total;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function findPackagePath(packageName: string, cwd: string): string | null {
|
|
75
|
+
const directPath = join(cwd, 'node_modules', packageName);
|
|
76
|
+
if (existsSync(directPath)) return directPath;
|
|
77
|
+
|
|
78
|
+
// Handle scoped packages
|
|
79
|
+
if (packageName.startsWith('@')) {
|
|
80
|
+
const parts = packageName.split('/');
|
|
81
|
+
const scopedPath = join(cwd, 'node_modules', parts[0], parts[1]);
|
|
82
|
+
if (existsSync(scopedPath)) return scopedPath;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getDependencyTree(packagePath: string, cwd: string, visited: Set<string> = new Set()): SizeBreakdown[] {
|
|
88
|
+
const breakdown: SizeBreakdown[] = [];
|
|
89
|
+
const pkgJsonPath = join(packagePath, 'package.json');
|
|
90
|
+
const pkg = readPackageJson(pkgJsonPath);
|
|
91
|
+
|
|
92
|
+
if (!pkg?.dependencies) return breakdown;
|
|
93
|
+
|
|
94
|
+
for (const depName of Object.keys(pkg.dependencies)) {
|
|
95
|
+
if (visited.has(depName)) continue;
|
|
96
|
+
visited.add(depName);
|
|
97
|
+
|
|
98
|
+
// Check nested node_modules first
|
|
99
|
+
let depPath = join(packagePath, 'node_modules', depName);
|
|
100
|
+
if (!existsSync(depPath)) {
|
|
101
|
+
depPath = join(cwd, 'node_modules', depName);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (existsSync(depPath)) {
|
|
105
|
+
const size = calculateJsSize(depPath);
|
|
106
|
+
if (size > 0) {
|
|
107
|
+
breakdown.push({ name: depName, size });
|
|
108
|
+
}
|
|
109
|
+
// Recursively get sub-dependencies
|
|
110
|
+
const subDeps = getDependencyTree(depPath, cwd, visited);
|
|
111
|
+
breakdown.push(...subDeps);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return breakdown;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function analyzeSizeMap(packageName: string, cwd: string = process.cwd()): SizeMapResult {
|
|
119
|
+
const packagePath = findPackagePath(packageName, cwd);
|
|
120
|
+
if (!packagePath) {
|
|
121
|
+
throw new Error(`Package "${packageName}" not found in node_modules`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const ownSize = calculateJsSize(packagePath);
|
|
125
|
+
const visited = new Set<string>([packageName]);
|
|
126
|
+
const depBreakdown = getDependencyTree(packagePath, cwd, visited);
|
|
127
|
+
|
|
128
|
+
const breakdown: SizeBreakdown[] = [
|
|
129
|
+
{ name: packageName, size: ownSize },
|
|
130
|
+
...depBreakdown.sort((a, b) => b.size - a.size)
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const totalSize = breakdown.reduce((sum, item) => sum + item.size, 0);
|
|
134
|
+
const nodeModulesSize = getNodeModulesSize(cwd);
|
|
135
|
+
const percentOfNodeModules = nodeModulesSize > 0 ? (totalSize / nodeModulesSize) * 100 : 0;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
packageName,
|
|
139
|
+
totalSize,
|
|
140
|
+
breakdown,
|
|
141
|
+
nodeModulesSize,
|
|
142
|
+
percentOfNodeModules
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
package/src/types.ts
CHANGED
|
@@ -4,20 +4,6 @@ export interface DependencyPath {
|
|
|
4
4
|
packageJsonPath?: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export interface FileUsage {
|
|
8
|
-
file: string;
|
|
9
|
-
lines: number[];
|
|
10
|
-
methods: string[];
|
|
11
|
-
context?: string[];
|
|
12
|
-
purpose?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ImpactAnalysis {
|
|
16
|
-
files: FileUsage[];
|
|
17
|
-
riskLevel: 'Low' | 'Medium' | 'High';
|
|
18
|
-
impacts: string[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
7
|
export interface PackageInfo {
|
|
22
8
|
name: string;
|
|
23
9
|
version: string;
|
|
@@ -25,7 +11,6 @@ export interface PackageInfo {
|
|
|
25
11
|
size?: number;
|
|
26
12
|
paths: DependencyPath[];
|
|
27
13
|
sourceFiles?: string[];
|
|
28
|
-
impact?: ImpactAnalysis;
|
|
29
14
|
}
|
|
30
15
|
|
|
31
16
|
export interface AnalyzeResult {
|
|
@@ -34,3 +19,16 @@ export interface AnalyzeResult {
|
|
|
34
19
|
}
|
|
35
20
|
|
|
36
21
|
export type PackageManager = 'npm' | 'yarn' | 'pnpm';
|
|
22
|
+
|
|
23
|
+
export interface SizeBreakdown {
|
|
24
|
+
name: string;
|
|
25
|
+
size: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SizeMapResult {
|
|
29
|
+
packageName: string;
|
|
30
|
+
totalSize: number;
|
|
31
|
+
breakdown: SizeBreakdown[];
|
|
32
|
+
nodeModulesSize?: number;
|
|
33
|
+
percentOfNodeModules?: number;
|
|
34
|
+
}
|