whyinstall 0.3.0 → 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/README.md CHANGED
@@ -24,6 +24,7 @@ whyinstall lodash
24
24
 
25
25
  - `-j, --json` - Output results as JSON
26
26
  - `-c, --cwd <path>` - Set working directory (default: current directory)
27
+ - `-s, --size-map` - Show bundle size impact breakdown
27
28
 
28
29
  ## Features
29
30
 
@@ -35,9 +36,14 @@ whyinstall lodash
35
36
  - Colored tree output for readability
36
37
  - JSON output for CI/CD
37
38
  - Actionable suggestions for optimization
39
+ - Bundle size impact breakdown
38
40
 
39
41
  ## Example Output
40
42
 
43
+ ```bash
44
+ whyinstall chalk
45
+ ```
46
+
41
47
  ```
42
48
  chalk v5.3.0 (43 KB)
43
49
  Terminal string styling done right
@@ -56,6 +62,33 @@ Suggested actions:
56
62
  1. Can be removed from direct dependencies - it's installed transitively
57
63
  ```
58
64
 
65
+ ### Size Map Output
66
+
67
+ ```bash
68
+ whyinstall next --size-map
69
+ ```
70
+
71
+ ```
72
+ Size map for: next
73
+
74
+ next total impact: 66.69 MB
75
+
76
+ Breakdown:
77
+ - next: 63.08 MB
78
+ - caniuse-lite: 2.19 MB
79
+ - styled-jsx: 971 KB
80
+ - @swc/helpers: 190 KB
81
+ - source-map-js: 104 KB
82
+ - postcss: 101 KB
83
+ - tslib: 60 KB
84
+ - nanoid: 20 KB
85
+ - @next/env: 10 KB
86
+ - picocolors: 3 KB
87
+ - client-only: 144 B
88
+
89
+ This package contributes 44.7% of your vendor bundle.
90
+ ```
91
+
59
92
  ## Development
60
93
 
61
94
  ```bash
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.0')
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.0",
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.0')
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
- }
@@ -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
- }
@@ -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
- }