whyinstall 0.3.1 → 0.3.2
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/dist/cli.js +1 -1
- package/package.json +11 -1
- package/src/analyzer.ts +0 -255
- package/src/cli.ts +0 -47
- package/src/fileFinder.ts +0 -78
- package/src/formatter.ts +0 -126
- package/src/packageManager.ts +0 -25
- package/src/sizeMapAnalyzer.ts +0 -145
- package/src/types.ts +0 -34
- package/tsconfig.json +0 -20
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ const program = new commander_1.Command();
|
|
|
14
14
|
program
|
|
15
15
|
.name('whyinstall')
|
|
16
16
|
.description('Find why a dependency exists in your JS/TS project')
|
|
17
|
-
.version('0.3.
|
|
17
|
+
.version('0.3.2')
|
|
18
18
|
.argument('<package-name>', 'Package name to analyze')
|
|
19
19
|
.option('-j, --json', 'Output as JSON')
|
|
20
20
|
.option('-c, --cwd <path>', 'Working directory', process.cwd())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whyinstall",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "CLI tool to find why a dependency exists in your JS/TS project",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,16 @@
|
|
|
21
21
|
],
|
|
22
22
|
"author": "Saumya Kushwah",
|
|
23
23
|
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/saumyakushwah/whyinstall.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://saumyakushwah.github.io/whyinstall",
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
24
34
|
"dependencies": {
|
|
25
35
|
"chalk": "^4.1.2",
|
|
26
36
|
"commander": "^11.1.0"
|
package/src/analyzer.ts
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
2
|
-
import { join, dirname } from 'path';
|
|
3
|
-
import { PackageInfo, DependencyPath, AnalyzeResult } from './types';
|
|
4
|
-
import { detectPackageManager } from './packageManager';
|
|
5
|
-
import { findFilesUsingPackage } from './fileFinder';
|
|
6
|
-
|
|
7
|
-
interface PackageJson {
|
|
8
|
-
name?: string;
|
|
9
|
-
version?: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
dependencies?: Record<string, string>;
|
|
12
|
-
devDependencies?: Record<string, string>;
|
|
13
|
-
peerDependencies?: Record<string, string>;
|
|
14
|
-
optionalDependencies?: Record<string, string>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function readPackageJson(path: string): PackageJson | null {
|
|
18
|
-
try {
|
|
19
|
-
if (existsSync(path)) {
|
|
20
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
21
|
-
}
|
|
22
|
-
} catch {
|
|
23
|
-
// ignore
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function findPackageJsonPath(packageName: string, cwd: string): string | null {
|
|
29
|
-
try {
|
|
30
|
-
const resolved = require.resolve(`${packageName}/package.json`, { paths: [cwd] });
|
|
31
|
-
return resolved;
|
|
32
|
-
} catch {
|
|
33
|
-
const nodeModulesPath = join(cwd, 'node_modules', packageName, 'package.json');
|
|
34
|
-
if (existsSync(nodeModulesPath)) {
|
|
35
|
-
return nodeModulesPath;
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function findPackageInNodeModules(packageName: string, basePath: string): string | null {
|
|
42
|
-
const possiblePaths = [
|
|
43
|
-
join(basePath, 'node_modules', packageName, 'package.json'),
|
|
44
|
-
join(basePath, packageName, 'package.json')
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
for (const path of possiblePaths) {
|
|
48
|
-
if (existsSync(path)) {
|
|
49
|
-
return path;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const parent = dirname(basePath);
|
|
54
|
-
if (parent !== basePath && parent !== '/') {
|
|
55
|
-
return findPackageInNodeModules(packageName, parent);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function findPackageSize(packagePath: string): number | undefined {
|
|
62
|
-
try {
|
|
63
|
-
const stats = statSync(packagePath);
|
|
64
|
-
if (stats.isDirectory()) {
|
|
65
|
-
let totalSize = 0;
|
|
66
|
-
const files = readdirSync(packagePath);
|
|
67
|
-
for (const file of files) {
|
|
68
|
-
const filePath = join(packagePath, file);
|
|
69
|
-
try {
|
|
70
|
-
const fileStats = statSync(filePath);
|
|
71
|
-
if (fileStats.isDirectory()) {
|
|
72
|
-
const subSize = findPackageSize(filePath);
|
|
73
|
-
if (subSize) totalSize += subSize;
|
|
74
|
-
} else {
|
|
75
|
-
totalSize += fileStats.size;
|
|
76
|
-
}
|
|
77
|
-
} catch {
|
|
78
|
-
// ignore
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return totalSize;
|
|
82
|
-
}
|
|
83
|
-
} catch {
|
|
84
|
-
// ignore
|
|
85
|
-
}
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function getAllDependencies(pkg: PackageJson): Record<string, 'prod' | 'dev' | 'peer' | 'optional'> {
|
|
90
|
-
const deps: Record<string, 'prod' | 'dev' | 'peer' | 'optional'> = {};
|
|
91
|
-
|
|
92
|
-
if (pkg.dependencies) {
|
|
93
|
-
for (const name of Object.keys(pkg.dependencies)) {
|
|
94
|
-
deps[name] = 'prod';
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (pkg.devDependencies) {
|
|
98
|
-
for (const name of Object.keys(pkg.devDependencies)) {
|
|
99
|
-
deps[name] = 'dev';
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (pkg.peerDependencies) {
|
|
103
|
-
for (const name of Object.keys(pkg.peerDependencies)) {
|
|
104
|
-
deps[name] = 'peer';
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (pkg.optionalDependencies) {
|
|
108
|
-
for (const name of Object.keys(pkg.optionalDependencies)) {
|
|
109
|
-
deps[name] = 'optional';
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return deps;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function findDependencyPaths(
|
|
117
|
-
targetPackage: string,
|
|
118
|
-
cwd: string,
|
|
119
|
-
maxDepth: number = 10
|
|
120
|
-
): DependencyPath[] {
|
|
121
|
-
const paths: DependencyPath[] = [];
|
|
122
|
-
const visited = new Set<string>();
|
|
123
|
-
|
|
124
|
-
interface QueueItem {
|
|
125
|
-
packageName: string;
|
|
126
|
-
chain: string[];
|
|
127
|
-
packageJsonPath: string;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const rootPackageJson = join(cwd, 'package.json');
|
|
131
|
-
if (!existsSync(rootPackageJson)) {
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const queue: QueueItem[] = [{ packageName: '', chain: [], packageJsonPath: rootPackageJson }];
|
|
136
|
-
|
|
137
|
-
while (queue.length > 0) {
|
|
138
|
-
const current = queue.shift()!;
|
|
139
|
-
|
|
140
|
-
if (current.chain.length > maxDepth) {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const pkg = readPackageJson(current.packageJsonPath);
|
|
145
|
-
if (!pkg) {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const allDeps = getAllDependencies(pkg);
|
|
150
|
-
const currentPackageName = current.packageName || pkg.name || '';
|
|
151
|
-
|
|
152
|
-
const visitKey = `${currentPackageName}:${current.packageJsonPath}`;
|
|
153
|
-
if (visited.has(visitKey)) {
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
visited.add(visitKey);
|
|
157
|
-
|
|
158
|
-
for (const [depName, depType] of Object.entries(allDeps)) {
|
|
159
|
-
if (depName === targetPackage) {
|
|
160
|
-
const chain = currentPackageName
|
|
161
|
-
? (current.chain.length > 0 ? [...current.chain, currentPackageName, depName] : [currentPackageName, depName])
|
|
162
|
-
: [depName];
|
|
163
|
-
paths.push({
|
|
164
|
-
chain,
|
|
165
|
-
type: depType,
|
|
166
|
-
packageJsonPath: current.packageJsonPath
|
|
167
|
-
});
|
|
168
|
-
} else {
|
|
169
|
-
const depPackageJsonPath = findPackageInNodeModules(depName, dirname(current.packageJsonPath));
|
|
170
|
-
if (depPackageJsonPath) {
|
|
171
|
-
const newChain = currentPackageName
|
|
172
|
-
? (current.chain.length > 0 ? [...current.chain, currentPackageName] : [currentPackageName])
|
|
173
|
-
: [];
|
|
174
|
-
queue.push({
|
|
175
|
-
packageName: depName,
|
|
176
|
-
chain: newChain,
|
|
177
|
-
packageJsonPath: depPackageJsonPath
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return paths;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function formatSize(bytes: number | undefined): string {
|
|
188
|
-
if (!bytes) return '';
|
|
189
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
190
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
191
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export function analyzePackage(packageName: string, cwd: string = process.cwd()): AnalyzeResult {
|
|
195
|
-
const rootPackageJson = join(cwd, 'package.json');
|
|
196
|
-
const packageJsonPath = findPackageJsonPath(packageName, cwd);
|
|
197
|
-
if (!packageJsonPath) {
|
|
198
|
-
throw new Error(`Package "${packageName}" not found in node_modules`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const packageDir = dirname(packageJsonPath);
|
|
202
|
-
const pkg = readPackageJson(packageJsonPath);
|
|
203
|
-
const version = pkg?.version || 'unknown';
|
|
204
|
-
const description = pkg?.description;
|
|
205
|
-
|
|
206
|
-
const size = findPackageSize(packageDir);
|
|
207
|
-
|
|
208
|
-
let paths = findDependencyPaths(packageName, cwd);
|
|
209
|
-
|
|
210
|
-
// Deduplicate paths with same chain and type
|
|
211
|
-
const pathKeys = new Set<string>();
|
|
212
|
-
paths = paths.filter(path => {
|
|
213
|
-
const key = `${path.type}:${path.chain.join('->')}`;
|
|
214
|
-
if (pathKeys.has(key)) {
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
pathKeys.add(key);
|
|
218
|
-
return true;
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const suggestions: string[] = [];
|
|
222
|
-
|
|
223
|
-
if (paths.length === 0) {
|
|
224
|
-
suggestions.push(`Package "${packageName}" is not in dependency tree`);
|
|
225
|
-
} else {
|
|
226
|
-
const devPaths = paths.filter(p => p.type === 'dev');
|
|
227
|
-
const peerPaths = paths.filter(p => p.type === 'peer');
|
|
228
|
-
|
|
229
|
-
if (devPaths.length > 0) {
|
|
230
|
-
suggestions.push(`Consider removing from devDependencies if not needed for development`);
|
|
231
|
-
}
|
|
232
|
-
if (peerPaths.length > 0) {
|
|
233
|
-
suggestions.push(`This is a peer dependency - ensure all consumers satisfy the peer requirement`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const directDeps = paths.filter(p => p.chain.length === 1 && p.packageJsonPath === rootPackageJson);
|
|
237
|
-
if (directDeps.length > 0 && paths.length > directDeps.length) {
|
|
238
|
-
suggestions.push(`Can be removed from direct dependencies - it's installed transitively`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const sourceFiles = findFilesUsingPackage(packageName, cwd);
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
package: {
|
|
246
|
-
name: packageName,
|
|
247
|
-
version,
|
|
248
|
-
description,
|
|
249
|
-
size,
|
|
250
|
-
paths,
|
|
251
|
-
sourceFiles: sourceFiles.length > 0 ? sourceFiles : undefined
|
|
252
|
-
},
|
|
253
|
-
suggestions
|
|
254
|
-
};
|
|
255
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { analyzePackage } from './analyzer';
|
|
6
|
-
import { formatOutput, formatSizeMap } from './formatter';
|
|
7
|
-
import { detectPackageManager } from './packageManager';
|
|
8
|
-
import { analyzeSizeMap } from './sizeMapAnalyzer';
|
|
9
|
-
|
|
10
|
-
const program = new Command();
|
|
11
|
-
|
|
12
|
-
program
|
|
13
|
-
.name('whyinstall')
|
|
14
|
-
.description('Find why a dependency exists in your JS/TS project')
|
|
15
|
-
.version('0.3.1')
|
|
16
|
-
.argument('<package-name>', 'Package name to analyze')
|
|
17
|
-
.option('-j, --json', 'Output as JSON')
|
|
18
|
-
.option('-c, --cwd <path>', 'Working directory', process.cwd())
|
|
19
|
-
.option('-s, --size-map', 'Show bundle size impact breakdown')
|
|
20
|
-
.action((packageName: string, options: { json?: boolean; cwd?: string; sizeMap?: boolean }) => {
|
|
21
|
-
try {
|
|
22
|
-
const cwd = options.cwd || process.cwd();
|
|
23
|
-
const pm = detectPackageManager(cwd);
|
|
24
|
-
|
|
25
|
-
if (!options.json) {
|
|
26
|
-
console.log(`\n${chalk.gray(`Detected package manager: ${pm}`)}\n`);
|
|
27
|
-
}
|
|
28
|
-
|
|
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
|
-
}
|
|
38
|
-
|
|
39
|
-
process.exit(0);
|
|
40
|
-
} catch (error) {
|
|
41
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
-
console.error(chalk.red(`Error: ${message}`));
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
program.parse();
|
package/src/fileFinder.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
2
|
-
import { join, extname } from 'path';
|
|
3
|
-
|
|
4
|
-
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
5
|
-
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];
|
|
6
|
-
|
|
7
|
-
function shouldIgnoreDir(dirName: string): boolean {
|
|
8
|
-
return IGNORE_DIRS.includes(dirName) || dirName.startsWith('.');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function isSourceFile(filePath: string): boolean {
|
|
12
|
-
return SOURCE_EXTENSIONS.includes(extname(filePath));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function findSourceFiles(dir: string, maxDepth: number = 5, currentDepth: number = 0): string[] {
|
|
16
|
-
if (currentDepth > maxDepth) {
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const files: string[] = [];
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const entries = readdirSync(dir);
|
|
24
|
-
|
|
25
|
-
for (const entry of entries) {
|
|
26
|
-
const fullPath = join(dir, entry);
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const stats = statSync(fullPath);
|
|
30
|
-
|
|
31
|
-
if (stats.isDirectory()) {
|
|
32
|
-
if (!shouldIgnoreDir(entry)) {
|
|
33
|
-
files.push(...findSourceFiles(fullPath, maxDepth, currentDepth + 1));
|
|
34
|
-
}
|
|
35
|
-
} else if (stats.isFile() && isSourceFile(fullPath)) {
|
|
36
|
-
files.push(fullPath);
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
39
|
-
// ignore errors for individual files/dirs
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
// ignore errors
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return files;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function fileContainsPackage(filePath: string, packageName: string): boolean {
|
|
50
|
-
try {
|
|
51
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
52
|
-
|
|
53
|
-
const patterns = [
|
|
54
|
-
new RegExp(`require\\(['"]${packageName}(/.*)?['"]\\)`, 'g'),
|
|
55
|
-
new RegExp(`from\\s+['"]${packageName}(/.*)?['"]`, 'g'),
|
|
56
|
-
new RegExp(`import\\s+.*\\s+from\\s+['"]${packageName}(/.*)?['"]`, 'g'),
|
|
57
|
-
new RegExp(`import\\s+['"]${packageName}(/.*)?['"]`, 'g'),
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
return patterns.some(pattern => pattern.test(content));
|
|
61
|
-
} catch {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function findFilesUsingPackage(packageName: string, cwd: string): string[] {
|
|
67
|
-
const sourceFiles = findSourceFiles(cwd);
|
|
68
|
-
const matchingFiles: string[] = [];
|
|
69
|
-
|
|
70
|
-
for (const file of sourceFiles) {
|
|
71
|
-
if (fileContainsPackage(file, packageName)) {
|
|
72
|
-
const relativePath = file.replace(cwd + '/', '');
|
|
73
|
-
matchingFiles.push(relativePath);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return matchingFiles;
|
|
78
|
-
}
|
package/src/formatter.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { AnalyzeResult, DependencyPath, SizeMapResult } from './types';
|
|
3
|
-
|
|
4
|
-
function formatSize(bytes: number | undefined): string {
|
|
5
|
-
if (!bytes) return '';
|
|
6
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
7
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
8
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function formatChain(chain: string[], isLast: boolean): string {
|
|
12
|
-
if (chain.length === 0) return '';
|
|
13
|
-
|
|
14
|
-
const parts: string[] = [];
|
|
15
|
-
|
|
16
|
-
for (let i = 0; i < chain.length; i++) {
|
|
17
|
-
const isLastInChain = i === chain.length - 1;
|
|
18
|
-
let prefix: string;
|
|
19
|
-
let indent = '';
|
|
20
|
-
|
|
21
|
-
if (i === 0) {
|
|
22
|
-
prefix = '';
|
|
23
|
-
indent = ' ';
|
|
24
|
-
} else {
|
|
25
|
-
indent = ' ' + ' '.repeat((i - 1) * 3);
|
|
26
|
-
if (isLastInChain) {
|
|
27
|
-
prefix = '└─> ';
|
|
28
|
-
} else {
|
|
29
|
-
prefix = '├─> ';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let packageName = chain[i];
|
|
34
|
-
if (packageName.includes('node_modules/')) {
|
|
35
|
-
packageName = chalk.gray(packageName.replace(/.*node_modules\//, 'node_modules/'));
|
|
36
|
-
} else if (packageName.includes('/')) {
|
|
37
|
-
packageName = chalk.white(packageName);
|
|
38
|
-
} else {
|
|
39
|
-
packageName = chalk.cyan(packageName);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
parts.push(`${indent}${prefix}${packageName}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return parts.join('\n');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function getTypeLabel(type: DependencyPath['type']): string {
|
|
49
|
-
const labels = {
|
|
50
|
-
prod: chalk.green('prod'),
|
|
51
|
-
dev: chalk.yellow('dev'),
|
|
52
|
-
peer: chalk.blue('peer'),
|
|
53
|
-
optional: chalk.gray('optional')
|
|
54
|
-
};
|
|
55
|
-
return labels[type] || type;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function formatOutput(result: AnalyzeResult, json: boolean = false): string {
|
|
59
|
-
if (json) {
|
|
60
|
-
return JSON.stringify(result, null, 2);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const { package: pkg, suggestions } = result;
|
|
64
|
-
const sizeStr = pkg.size ? formatSize(pkg.size) : '';
|
|
65
|
-
const sizeDisplay = sizeStr ? ` (${sizeStr})` : '';
|
|
66
|
-
|
|
67
|
-
let output = '';
|
|
68
|
-
output += chalk.bold.cyan(`${pkg.name}`) + chalk.gray(` v${pkg.version}`) + chalk.dim(`${sizeDisplay}`) + '\n';
|
|
69
|
-
if (pkg.description) {
|
|
70
|
-
output += chalk.white(` ${pkg.description}\n`);
|
|
71
|
-
}
|
|
72
|
-
output += '\n' + chalk.dim(` installed via ${pkg.paths.length} path${pkg.paths.length !== 1 ? 's' : ''}\n\n`);
|
|
73
|
-
|
|
74
|
-
if (pkg.paths.length === 0) {
|
|
75
|
-
output += chalk.yellow(' No dependency paths found.\n');
|
|
76
|
-
} else {
|
|
77
|
-
pkg.paths.forEach((path, index) => {
|
|
78
|
-
const pathNum = chalk.gray(`${index + 1}.`);
|
|
79
|
-
const typeLabel = getTypeLabel(path.type);
|
|
80
|
-
|
|
81
|
-
let chainDisplay = path.chain.length > 0 ? path.chain : [pkg.name];
|
|
82
|
-
|
|
83
|
-
output += `${pathNum} ${typeLabel}\n`;
|
|
84
|
-
output += formatChain(chainDisplay, index === pkg.paths.length - 1);
|
|
85
|
-
output += '\n';
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (pkg.sourceFiles && pkg.sourceFiles.length > 0) {
|
|
90
|
-
output += '\n' + chalk.bold(`Used in (${pkg.sourceFiles.length}):`) + '\n';
|
|
91
|
-
pkg.sourceFiles.forEach((file) => {
|
|
92
|
-
output += ` ${chalk.blue(file)}\n`;
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (suggestions.length > 0) {
|
|
97
|
-
output += '\n' + chalk.bold('Suggested actions:') + '\n';
|
|
98
|
-
suggestions.forEach((suggestion, index) => {
|
|
99
|
-
output += ` ${index + 1}. ${chalk.gray(suggestion)}\n`;
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return output;
|
|
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
|
-
}
|
package/src/packageManager.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { PackageManager } from './types';
|
|
4
|
-
|
|
5
|
-
export function detectPackageManager(cwd: string = process.cwd()): PackageManager {
|
|
6
|
-
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) {
|
|
7
|
-
return 'pnpm';
|
|
8
|
-
}
|
|
9
|
-
if (existsSync(join(cwd, 'yarn.lock'))) {
|
|
10
|
-
return 'yarn';
|
|
11
|
-
}
|
|
12
|
-
if (existsSync(join(cwd, 'package-lock.json'))) {
|
|
13
|
-
return 'npm';
|
|
14
|
-
}
|
|
15
|
-
return 'npm';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function getLockFilePath(cwd: string, pm: PackageManager): string {
|
|
19
|
-
const paths = {
|
|
20
|
-
npm: 'package-lock.json',
|
|
21
|
-
yarn: 'yarn.lock',
|
|
22
|
-
pnpm: 'pnpm-lock.yaml'
|
|
23
|
-
};
|
|
24
|
-
return join(cwd, paths[pm]);
|
|
25
|
-
}
|
package/src/sizeMapAnalyzer.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export interface DependencyPath {
|
|
2
|
-
chain: string[];
|
|
3
|
-
type: 'prod' | 'dev' | 'peer' | 'optional';
|
|
4
|
-
packageJsonPath?: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface PackageInfo {
|
|
8
|
-
name: string;
|
|
9
|
-
version: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
size?: number;
|
|
12
|
-
paths: DependencyPath[];
|
|
13
|
-
sourceFiles?: string[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface AnalyzeResult {
|
|
17
|
-
package: PackageInfo;
|
|
18
|
-
suggestions: string[];
|
|
19
|
-
}
|
|
20
|
-
|
|
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
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "commonjs",
|
|
5
|
-
"lib": ["ES2020"],
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"moduleResolution": "node",
|
|
14
|
-
"declaration": true,
|
|
15
|
-
"declarationMap": true,
|
|
16
|
-
"sourceMap": true
|
|
17
|
-
},
|
|
18
|
-
"include": ["src/**/*"],
|
|
19
|
-
"exclude": ["node_modules", "dist"]
|
|
20
|
-
}
|