@saismrutiranjan18/why-slow 1.0.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.
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { scanProject } from "../src/scanner.js";
3
+
4
+ const args = process.argv.slice(2);
5
+
6
+ if (args.includes("--help") || args.includes("-h")) {
7
+ console.log(`
8
+ Usage: why-slow [options]
9
+
10
+ Options:
11
+ --html Generate an HTML report (performance-report.html)
12
+ --help Show this help message
13
+ `);
14
+ process.exit(0);
15
+ }
16
+
17
+ await scanProject({
18
+ html: args.includes("--html"),
19
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@saismrutiranjan18/why-slow",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to diagnose why your Node.js project is slow — scans for large files, oversized images, circular dependencies, and unused packages.",
5
+ "type": "module",
6
+ "bin": {
7
+ "why-slow": "./bin/why-slow.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md"
13
+ ],
14
+ "keywords": [
15
+ "performance",
16
+ "cli",
17
+ "audit",
18
+ "circular-dependencies",
19
+ "unused-dependencies",
20
+ "bundle-size",
21
+ "why-slow"
22
+ ],
23
+ "author": "saismrutiranjan18",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/saismrutiranjan18/npm-workspace"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "dependencies": {
33
+ "chalk": "^5.6.2",
34
+ "depcheck": "^1.4.7",
35
+ "fast-glob": "^3.3.3",
36
+ "fs-extra": "^11.3.5",
37
+ "madge": "^8.0.0"
38
+ }
39
+ }
package/src/scanner.js ADDED
@@ -0,0 +1,316 @@
1
+ import fg from "fast-glob";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import chalk from "chalk";
5
+ import madge from "madge";
6
+ import depcheck from "depcheck";
7
+ import fsExtra from "fs-extra";
8
+
9
+ const ICONS = {
10
+ info: chalk.blue("i"),
11
+ pass: chalk.green("✓"),
12
+ warn: chalk.yellow("!"),
13
+ error: chalk.red("✖"),
14
+ };
15
+
16
+ // FIX #3: Factory function instead of module-level state,
17
+ // so calling scanProject() twice never accumulates stale data.
18
+ function createReport() {
19
+ return {
20
+ largeFiles: [],
21
+ largeImages: [],
22
+ circularDeps: [],
23
+ unusedDeps: [],
24
+ nodeModulesSize: 0,
25
+ };
26
+ }
27
+
28
+ export async function scanProject(options = {}) {
29
+ console.clear();
30
+
31
+ console.log(chalk.bold.cyan("\nWHY-SLOW v1.1"));
32
+ console.log(chalk.gray("────────────────────────────────────"));
33
+
34
+ // FIX #3: fresh report each run
35
+ const report = createReport();
36
+
37
+ await scanLargeFiles(report);
38
+ await scanImages(report);
39
+ await scanCircularDependencies(report);
40
+ await scanUnusedDependencies(report);
41
+ await scanNodeModules(report);
42
+
43
+ printSummary(report);
44
+
45
+ if (options.html) {
46
+ await generateHtmlReport(report);
47
+ }
48
+ }
49
+
50
+ async function scanLargeFiles(report) {
51
+ console.log(`\n${ICONS.info} ${chalk.bold("Largest Source Files")}`);
52
+
53
+ const files = await fg(["src/**/*.{js,jsx,ts,tsx}"], {
54
+ cwd: process.cwd(),
55
+ absolute: true,
56
+ });
57
+
58
+ const results = [];
59
+
60
+ for (const file of files) {
61
+ try {
62
+ const size = fs.statSync(file).size;
63
+ results.push({ file, size });
64
+ } catch {
65
+ // skip unreadable files
66
+ }
67
+ }
68
+
69
+ results.sort((a, b) => b.size - a.size);
70
+
71
+ const topFiles = results.slice(0, 10);
72
+
73
+ for (const { file, size } of topFiles) {
74
+ const kb = (size / 1024).toFixed(1);
75
+
76
+ if (size > 200 * 1024) {
77
+ report.largeFiles.push({ file, size: kb });
78
+ console.log(`${ICONS.warn} ${file} (${kb} KB)`);
79
+ }
80
+ }
81
+
82
+ if (report.largeFiles.length === 0) {
83
+ console.log(`${ICONS.pass} No oversized source files found`);
84
+ }
85
+ }
86
+
87
+ async function scanImages(report) {
88
+ console.log(`\n${ICONS.info} ${chalk.bold("Image Analysis")}`);
89
+
90
+ const images = await fg(["**/*.{png,jpg,jpeg,webp,svg}"], {
91
+ ignore: ["node_modules/**"],
92
+ cwd: process.cwd(),
93
+ absolute: true,
94
+ });
95
+
96
+ let found = false;
97
+
98
+ for (const image of images) {
99
+ try {
100
+ const size = fs.statSync(image).size;
101
+
102
+ if (size > 500 * 1024) {
103
+ found = true;
104
+ const mb = (size / 1024 / 1024).toFixed(2);
105
+ report.largeImages.push({ file: image, size: mb });
106
+ console.log(`${ICONS.warn} ${image} (${mb} MB)`);
107
+ }
108
+ } catch {
109
+ // skip unreadable files
110
+ }
111
+ }
112
+
113
+ if (!found) {
114
+ console.log(`${ICONS.pass} No oversized images found`);
115
+ }
116
+ }
117
+
118
+ async function scanCircularDependencies(report) {
119
+ console.log(`\n${ICONS.info} ${chalk.bold("Circular Dependencies")}`);
120
+
121
+ try {
122
+ // FIX #4: use absolute path so madge works regardless of cwd
123
+ const srcPath = path.join(process.cwd(), "src");
124
+
125
+ if (!fs.existsSync(srcPath)) {
126
+ console.log(`${ICONS.warn} No src/ directory found, skipping`);
127
+ return;
128
+ }
129
+
130
+ const result = await madge(srcPath);
131
+ const circular = result.circular();
132
+
133
+ report.circularDeps = circular;
134
+
135
+ if (!circular.length) {
136
+ console.log(`${ICONS.pass} No circular dependencies found`);
137
+ return;
138
+ }
139
+
140
+ circular.forEach((dep) => {
141
+ console.log(`${ICONS.error} ${dep.join(" → ")}`);
142
+ });
143
+ } catch (error) {
144
+ console.log(`${ICONS.warn} Unable to analyze circular dependencies`);
145
+ }
146
+ }
147
+
148
+ async function scanUnusedDependencies(report) {
149
+ console.log(`\n${ICONS.info} ${chalk.bold("Unused Dependencies")}`);
150
+
151
+ try {
152
+ const result = await depcheck(process.cwd(), {});
153
+
154
+ // FIX #2: depcheck returns { dependencies: [...], devDependencies: [...] }
155
+ // at the top level — but the shape is an object of unused dep names.
156
+ // result.dependencies is an array of unused production dep names.
157
+ const unused = Array.isArray(result.dependencies) ? result.dependencies : [];
158
+
159
+ report.unusedDeps = unused;
160
+
161
+ if (!unused.length) {
162
+ console.log(`${ICONS.pass} No unused dependencies found`);
163
+ return;
164
+ }
165
+
166
+ unused.forEach((dep) => {
167
+ console.log(`${ICONS.warn} ${dep}`);
168
+ });
169
+ } catch (error) {
170
+ console.log(`${ICONS.warn} Unable to analyze dependencies`);
171
+ }
172
+ }
173
+
174
+ async function scanNodeModules(report) {
175
+ console.log(`\n${ICONS.info} ${chalk.bold("Dependencies")}`);
176
+
177
+ const nodeModules = path.join(process.cwd(), "node_modules");
178
+
179
+ if (!fs.existsSync(nodeModules)) {
180
+ console.log(`${ICONS.warn} node_modules not found`);
181
+ return;
182
+ }
183
+
184
+ const size = getFolderSize(nodeModules);
185
+ report.nodeModulesSize = (size / 1024 / 1024).toFixed(2);
186
+
187
+ console.log(
188
+ `${ICONS.info} node_modules size: ${report.nodeModulesSize} MB`
189
+ );
190
+ }
191
+
192
+ // FIX #1: guard against symlinks and permission errors that crash statSync
193
+ function getFolderSize(folder) {
194
+ let total = 0;
195
+
196
+ let files;
197
+ try {
198
+ files = fs.readdirSync(folder);
199
+ } catch {
200
+ return 0;
201
+ }
202
+
203
+ for (const file of files) {
204
+ const fullPath = path.join(folder, file);
205
+
206
+ try {
207
+ // lstatSync instead of statSync — does NOT follow symlinks,
208
+ // so broken symlinks in node_modules no longer throw.
209
+ const stat = fs.lstatSync(fullPath);
210
+
211
+ if (stat.isSymbolicLink()) {
212
+ // skip symlinks entirely; they don't contribute real disk usage
213
+ continue;
214
+ }
215
+
216
+ if (stat.isDirectory()) {
217
+ total += getFolderSize(fullPath);
218
+ } else {
219
+ total += stat.size;
220
+ }
221
+ } catch {
222
+ // skip any entry that can't be stat'd
223
+ }
224
+ }
225
+
226
+ return total;
227
+ }
228
+
229
+ function printSummary(report) {
230
+ let score = 100;
231
+
232
+ score -= report.largeFiles.length * 5;
233
+ score -= report.largeImages.length * 5;
234
+ score -= report.circularDeps.length * 10;
235
+ score -= report.unusedDeps.length * 2;
236
+
237
+ score = Math.max(score, 0);
238
+
239
+ console.log("\n" + chalk.gray("────────────────────────────────────"));
240
+ console.log(chalk.bold("\nPerformance Summary\n"));
241
+
242
+ console.log(`${ICONS.info} Large Files: ${report.largeFiles.length}`);
243
+ console.log(`${ICONS.info} Large Images: ${report.largeImages.length}`);
244
+ console.log(`${ICONS.info} Circular Dependencies: ${report.circularDeps.length}`);
245
+ console.log(`${ICONS.info} Unused Dependencies: ${report.unusedDeps.length}`);
246
+
247
+ console.log();
248
+
249
+ if (score >= 90) {
250
+ console.log(chalk.green(`${ICONS.pass} Performance Score: ${score}/100`));
251
+ } else if (score >= 70) {
252
+ console.log(chalk.yellow(`${ICONS.warn} Performance Score: ${score}/100`));
253
+ } else {
254
+ console.log(chalk.red(`${ICONS.error} Performance Score: ${score}/100`));
255
+ }
256
+
257
+ console.log(chalk.gray("\n────────────────────────────────────"));
258
+ console.log(chalk.bold("\nScan completed successfully.\n"));
259
+ }
260
+
261
+ async function generateHtmlReport(report) {
262
+ const html = `<!DOCTYPE html>
263
+ <html>
264
+ <head>
265
+ <meta charset="UTF-8">
266
+ <title>WHY-SLOW Report</title>
267
+ <style>
268
+ body { font-family: Arial, sans-serif; padding: 40px; max-width: 1200px; margin: auto; background: #f5f5f5; }
269
+ .card { background: white; padding: 20px; margin-bottom: 20px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,.1); }
270
+ h1 { margin-bottom: 30px; }
271
+ ul { padding-left: 20px; }
272
+ .empty { color: #888; font-style: italic; }
273
+ </style>
274
+ </head>
275
+ <body>
276
+ <h1>WHY-SLOW Performance Report</h1>
277
+
278
+ <div class="card">
279
+ <h2>Node Modules Size</h2>
280
+ <p>${report.nodeModulesSize} MB</p>
281
+ </div>
282
+
283
+ <div class="card">
284
+ <h2>Large Files</h2>
285
+ ${report.largeFiles.length
286
+ ? `<ul>${report.largeFiles.map(f => `<li>${f.file} — ${f.size} KB</li>`).join("")}</ul>`
287
+ : `<p class="empty">None found</p>`}
288
+ </div>
289
+
290
+ <div class="card">
291
+ <h2>Large Images</h2>
292
+ ${report.largeImages.length
293
+ ? `<ul>${report.largeImages.map(i => `<li>${i.file} — ${i.size} MB</li>`).join("")}</ul>`
294
+ : `<p class="empty">None found</p>`}
295
+ </div>
296
+
297
+ <div class="card">
298
+ <h2>Circular Dependencies</h2>
299
+ ${report.circularDeps.length
300
+ ? `<ul>${report.circularDeps.map(c => `<li>${c.join(" → ")}</li>`).join("")}</ul>`
301
+ : `<p class="empty">None found</p>`}
302
+ </div>
303
+
304
+ <div class="card">
305
+ <h2>Unused Dependencies</h2>
306
+ ${report.unusedDeps.length
307
+ ? `<ul>${report.unusedDeps.map(d => `<li>${d}</li>`).join("")}</ul>`
308
+ : `<p class="empty">None found</p>`}
309
+ </div>
310
+
311
+ </body>
312
+ </html>`;
313
+
314
+ await fsExtra.writeFile("performance-report.html", html);
315
+ console.log(chalk.green("\n✓ HTML report generated: performance-report.html\n"));
316
+ }