prunify 0.1.1 → 0.1.4
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 +166 -99
- package/dist/cli.cjs +485 -158
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +485 -158
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
5
|
+
import chalk7 from "chalk";
|
|
6
|
+
import Table6 from "cli-table3";
|
|
7
|
+
import fs9 from "fs";
|
|
8
|
+
import path9 from "path";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import readline from "readline";
|
|
11
11
|
|
|
@@ -57,7 +57,14 @@ var DEFAULT_IGNORE = [
|
|
|
57
57
|
"coverage",
|
|
58
58
|
"coverage/**",
|
|
59
59
|
"**/*.test.ts",
|
|
60
|
+
"**/*.test.tsx",
|
|
61
|
+
"**/*.test.js",
|
|
62
|
+
"**/*.test.jsx",
|
|
60
63
|
"**/*.spec.ts",
|
|
64
|
+
"**/*.spec.tsx",
|
|
65
|
+
"**/*.spec.js",
|
|
66
|
+
"**/*.spec.jsx",
|
|
67
|
+
"**/*.stories.ts",
|
|
61
68
|
"**/*.stories.tsx",
|
|
62
69
|
"**/*.d.ts"
|
|
63
70
|
];
|
|
@@ -202,45 +209,22 @@ function buildGraph(files, getImports) {
|
|
|
202
209
|
function findEntryPoints(rootDir, packageJson) {
|
|
203
210
|
const entries = [
|
|
204
211
|
...resolveNextJsEntries(rootDir),
|
|
205
|
-
...
|
|
212
|
+
...resolveFileBrowserEntries(rootDir),
|
|
213
|
+
...resolvePkgFieldEntries(rootDir, packageJson),
|
|
214
|
+
...resolveFallbackEntries(rootDir)
|
|
206
215
|
];
|
|
207
|
-
if (entries.length === 0) {
|
|
208
|
-
const fallback = resolveFallbackEntry(rootDir);
|
|
209
|
-
if (fallback) entries.push(fallback);
|
|
210
|
-
}
|
|
211
216
|
return [...new Set(entries)];
|
|
212
217
|
}
|
|
213
|
-
function runDFS(graph, entryPoints) {
|
|
214
|
-
const visited = /* @__PURE__ */ new Set();
|
|
215
|
-
const stack = [...entryPoints];
|
|
216
|
-
let node;
|
|
217
|
-
while ((node = stack.pop()) !== void 0) {
|
|
218
|
-
if (visited.has(node)) continue;
|
|
219
|
-
visited.add(node);
|
|
220
|
-
for (const neighbor of graph.get(node) ?? []) {
|
|
221
|
-
if (!visited.has(neighbor)) stack.push(neighbor);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return visited;
|
|
225
|
-
}
|
|
226
|
-
function findDeadChains(graph, deadFiles) {
|
|
227
|
-
const reverseGraph = buildReverseGraph(graph);
|
|
228
|
-
const result = /* @__PURE__ */ new Map();
|
|
229
|
-
for (const deadRoot of deadFiles) {
|
|
230
|
-
result.set(deadRoot, dfsDeadChain(deadRoot, graph, deadFiles, reverseGraph));
|
|
231
|
-
}
|
|
232
|
-
return result;
|
|
233
|
-
}
|
|
234
218
|
function detectCycles(graph) {
|
|
235
219
|
const cycles = [];
|
|
236
220
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
237
221
|
const visited = /* @__PURE__ */ new Set();
|
|
238
222
|
const inStack = /* @__PURE__ */ new Set();
|
|
239
|
-
const
|
|
223
|
+
const path10 = [];
|
|
240
224
|
const acc = { seenKeys, cycles };
|
|
241
225
|
for (const start of graph.keys()) {
|
|
242
226
|
if (!visited.has(start)) {
|
|
243
|
-
dfsForCycles(start, graph, visited, inStack,
|
|
227
|
+
dfsForCycles(start, graph, visited, inStack, path10, acc);
|
|
244
228
|
}
|
|
245
229
|
}
|
|
246
230
|
return cycles;
|
|
@@ -249,12 +233,21 @@ function resolveNextJsEntries(rootDir) {
|
|
|
249
233
|
const isNext = fs3.existsSync(path3.join(rootDir, "next.config.js")) || fs3.existsSync(path3.join(rootDir, "next.config.ts")) || fs3.existsSync(path3.join(rootDir, "next.config.mjs"));
|
|
250
234
|
if (!isNext) return [];
|
|
251
235
|
const entries = [];
|
|
252
|
-
for (const dir of ["pages", "app"]) {
|
|
236
|
+
for (const dir of ["pages", "app", "src/pages", "src/app"]) {
|
|
253
237
|
const dirPath = path3.join(rootDir, dir);
|
|
254
238
|
if (fs3.existsSync(dirPath)) entries.push(...collectSourceFiles(dirPath));
|
|
255
239
|
}
|
|
256
240
|
return entries;
|
|
257
241
|
}
|
|
242
|
+
function resolveFileBrowserEntries(rootDir) {
|
|
243
|
+
const PAGE_DIRS = ["src/pages", "src/routes", "src/views", "src/screens"];
|
|
244
|
+
const entries = [];
|
|
245
|
+
for (const rel of PAGE_DIRS) {
|
|
246
|
+
const dirPath = path3.join(rootDir, rel);
|
|
247
|
+
if (fs3.existsSync(dirPath)) entries.push(...collectSourceFiles(dirPath));
|
|
248
|
+
}
|
|
249
|
+
return entries;
|
|
250
|
+
}
|
|
258
251
|
function resolvePkgFieldEntries(rootDir, packageJson) {
|
|
259
252
|
const entries = [];
|
|
260
253
|
for (const field of ["main", "module"]) {
|
|
@@ -265,18 +258,31 @@ function resolvePkgFieldEntries(rootDir, packageJson) {
|
|
|
265
258
|
}
|
|
266
259
|
return entries;
|
|
267
260
|
}
|
|
268
|
-
function
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
261
|
+
function resolveFallbackEntries(rootDir) {
|
|
262
|
+
const candidates = [
|
|
263
|
+
"src/main.ts",
|
|
264
|
+
"src/main.tsx",
|
|
265
|
+
"src/main.js",
|
|
266
|
+
"src/main.jsx",
|
|
267
|
+
"src/index.ts",
|
|
268
|
+
"src/index.tsx",
|
|
269
|
+
"src/index.js",
|
|
270
|
+
"src/index.jsx",
|
|
271
|
+
"src/App.ts",
|
|
272
|
+
"src/App.tsx",
|
|
273
|
+
"src/App.js",
|
|
274
|
+
"src/App.jsx",
|
|
275
|
+
"index.ts",
|
|
276
|
+
"index.tsx",
|
|
277
|
+
"index.js",
|
|
278
|
+
"index.jsx"
|
|
279
|
+
];
|
|
280
|
+
return candidates.map((rel) => path3.join(rootDir, rel)).filter((abs) => fs3.existsSync(abs));
|
|
275
281
|
}
|
|
276
282
|
function mkFrame(node, graph) {
|
|
277
283
|
return { node, neighbors: (graph.get(node) ?? /* @__PURE__ */ new Set()).values(), entered: false };
|
|
278
284
|
}
|
|
279
|
-
function dfsForCycles(start, graph, visited, inStack,
|
|
285
|
+
function dfsForCycles(start, graph, visited, inStack, path10, acc) {
|
|
280
286
|
const stack = [mkFrame(start, graph)];
|
|
281
287
|
while (stack.length > 0) {
|
|
282
288
|
const frame = stack.at(-1);
|
|
@@ -288,57 +294,36 @@ function dfsForCycles(start, graph, visited, inStack, path9, acc) {
|
|
|
288
294
|
}
|
|
289
295
|
frame.entered = true;
|
|
290
296
|
inStack.add(frame.node);
|
|
291
|
-
|
|
297
|
+
path10.push(frame.node);
|
|
292
298
|
}
|
|
293
299
|
const { done, value: neighbor } = frame.neighbors.next();
|
|
294
300
|
if (done) {
|
|
295
301
|
stack.pop();
|
|
296
|
-
|
|
302
|
+
path10.pop();
|
|
297
303
|
inStack.delete(frame.node);
|
|
298
304
|
visited.add(frame.node);
|
|
299
305
|
} else {
|
|
300
|
-
handleCycleNeighbor(neighbor, stack,
|
|
306
|
+
handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph);
|
|
301
307
|
}
|
|
302
308
|
}
|
|
303
309
|
}
|
|
304
|
-
function handleCycleNeighbor(neighbor, stack,
|
|
310
|
+
function handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph) {
|
|
305
311
|
if (inStack.has(neighbor)) {
|
|
306
|
-
recordCycle(neighbor,
|
|
312
|
+
recordCycle(neighbor, path10, acc);
|
|
307
313
|
} else if (!visited.has(neighbor)) {
|
|
308
314
|
stack.push(mkFrame(neighbor, graph));
|
|
309
315
|
}
|
|
310
316
|
}
|
|
311
|
-
function recordCycle(cycleStart,
|
|
312
|
-
const idx =
|
|
317
|
+
function recordCycle(cycleStart, path10, acc) {
|
|
318
|
+
const idx = path10.indexOf(cycleStart);
|
|
313
319
|
if (idx === -1) return;
|
|
314
|
-
const cycle = normalizeCycle(
|
|
320
|
+
const cycle = normalizeCycle(path10.slice(idx));
|
|
315
321
|
const key = cycle.join("\0");
|
|
316
322
|
if (!acc.seenKeys.has(key)) {
|
|
317
323
|
acc.seenKeys.add(key);
|
|
318
324
|
acc.cycles.push(cycle);
|
|
319
325
|
}
|
|
320
326
|
}
|
|
321
|
-
function dfsDeadChain(deadRoot, graph, deadFiles, reverseGraph) {
|
|
322
|
-
const chain = [];
|
|
323
|
-
const visited = /* @__PURE__ */ new Set();
|
|
324
|
-
const stack = [...graph.get(deadRoot) ?? []];
|
|
325
|
-
let node;
|
|
326
|
-
while ((node = stack.pop()) !== void 0) {
|
|
327
|
-
if (visited.has(node) || node === deadRoot) continue;
|
|
328
|
-
visited.add(node);
|
|
329
|
-
if (deadFiles.has(node) || isOnlyImportedByDead(node, deadFiles, reverseGraph)) {
|
|
330
|
-
chain.push(node);
|
|
331
|
-
for (const next of graph.get(node) ?? []) {
|
|
332
|
-
if (!visited.has(next)) stack.push(next);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return chain;
|
|
337
|
-
}
|
|
338
|
-
function isOnlyImportedByDead(file, deadFiles, reverseGraph) {
|
|
339
|
-
const importers = reverseGraph.get(file) ?? /* @__PURE__ */ new Set();
|
|
340
|
-
return importers.size === 0 || [...importers].every((imp) => deadFiles.has(imp));
|
|
341
|
-
}
|
|
342
327
|
function buildReverseGraph(graph) {
|
|
343
328
|
const rev = /* @__PURE__ */ new Map();
|
|
344
329
|
for (const [file] of graph) {
|
|
@@ -465,16 +450,101 @@ function ensureDir(dir) {
|
|
|
465
450
|
}
|
|
466
451
|
|
|
467
452
|
// src/modules/dead-code.ts
|
|
453
|
+
var FRAMEWORK_FILE_PATTERNS = [
|
|
454
|
+
/^next\.config\.(js|ts|mjs|cjs)$/,
|
|
455
|
+
/^middleware\.(ts|js)$/,
|
|
456
|
+
/^instrumentation\.(ts|js)$/,
|
|
457
|
+
/^tailwind\.config\.(js|ts|mjs|cjs)$/,
|
|
458
|
+
/^postcss\.config\.(js|ts|mjs|cjs)$/,
|
|
459
|
+
/^jest\.config\.(js|ts|mjs|cjs)$/,
|
|
460
|
+
/^vitest\.config\.(js|ts|mjs|cjs)$/,
|
|
461
|
+
/^vite\.config\.(js|ts|mjs|cjs)$/,
|
|
462
|
+
/^webpack\.config\.(js|ts|mjs|cjs)$/,
|
|
463
|
+
/^babel\.config\.(js|ts|cjs|mjs|json)$/,
|
|
464
|
+
/^\.babelrc\.(js|cjs)$/,
|
|
465
|
+
/^\.eslintrc\.(js|cjs)$/,
|
|
466
|
+
/^eslint\.config\.(js|ts|mjs|cjs)$/,
|
|
467
|
+
/^prettier\.config\.(js|ts|mjs|cjs)$/,
|
|
468
|
+
/^tsup\.config\.(ts|js)$/,
|
|
469
|
+
/^rollup\.config\.(js|ts|mjs)$/,
|
|
470
|
+
/^esbuild\.config\.(js|ts|mjs)$/,
|
|
471
|
+
/^commitlint\.config\.(js|ts)$/,
|
|
472
|
+
/^lint-staged\.config\.(js|ts|mjs|cjs)$/,
|
|
473
|
+
/^sentry\.(client|server|edge)\.config\.(ts|js)$/
|
|
474
|
+
];
|
|
475
|
+
function isFrameworkFile(filePath, rootDir) {
|
|
476
|
+
const rel = path5.relative(rootDir, filePath);
|
|
477
|
+
const basename = path5.basename(rel);
|
|
478
|
+
return FRAMEWORK_FILE_PATTERNS.some((re) => re.test(basename));
|
|
479
|
+
}
|
|
468
480
|
function runDeadCodeModule(project, graph, entryPoints, rootDir) {
|
|
469
481
|
const allFiles = [...graph.keys()];
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
const
|
|
474
|
-
|
|
482
|
+
const entrySet = new Set(entryPoints);
|
|
483
|
+
const reverseGraph = buildReverseGraph(graph);
|
|
484
|
+
const excludedFiles = /* @__PURE__ */ new Set();
|
|
485
|
+
for (const file of allFiles) {
|
|
486
|
+
if (isFrameworkFile(file, rootDir)) excludedFiles.add(file);
|
|
487
|
+
}
|
|
488
|
+
const safeToDelete = allFiles.filter((f) => {
|
|
489
|
+
if (entrySet.has(f) || excludedFiles.has(f)) return false;
|
|
490
|
+
const importers = reverseGraph.get(f);
|
|
491
|
+
return !importers || importers.size === 0;
|
|
492
|
+
});
|
|
493
|
+
const deadSet = new Set(safeToDelete);
|
|
494
|
+
let changed = true;
|
|
495
|
+
while (changed) {
|
|
496
|
+
changed = false;
|
|
497
|
+
for (const file of allFiles) {
|
|
498
|
+
if (deadSet.has(file) || entrySet.has(file) || excludedFiles.has(file)) continue;
|
|
499
|
+
const importers = reverseGraph.get(file);
|
|
500
|
+
if (!importers || importers.size === 0) continue;
|
|
501
|
+
if ([...importers].every((imp) => deadSet.has(imp))) {
|
|
502
|
+
deadSet.add(file);
|
|
503
|
+
changed = true;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const transitivelyDead = [...deadSet].filter((f) => !safeToDelete.includes(f));
|
|
508
|
+
const chains = /* @__PURE__ */ new Map();
|
|
509
|
+
for (const root of safeToDelete) {
|
|
510
|
+
const chain = collectTransitiveChain(root, graph, deadSet);
|
|
511
|
+
chains.set(root, chain);
|
|
512
|
+
}
|
|
513
|
+
const liveFiles = new Set(allFiles.filter((f) => !deadSet.has(f)));
|
|
475
514
|
const deadExports = findDeadExports(project, liveFiles);
|
|
476
|
-
const report = buildDeadCodeReport(
|
|
477
|
-
|
|
515
|
+
const report = buildDeadCodeReport(
|
|
516
|
+
safeToDelete,
|
|
517
|
+
transitivelyDead,
|
|
518
|
+
chains,
|
|
519
|
+
deadExports,
|
|
520
|
+
rootDir
|
|
521
|
+
);
|
|
522
|
+
return {
|
|
523
|
+
safeToDelete,
|
|
524
|
+
transitivelyDead,
|
|
525
|
+
deadFiles: [...deadSet],
|
|
526
|
+
liveFiles,
|
|
527
|
+
chains,
|
|
528
|
+
deadExports,
|
|
529
|
+
report
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function collectTransitiveChain(root, graph, deadSet) {
|
|
533
|
+
const chain = [];
|
|
534
|
+
const visited = /* @__PURE__ */ new Set();
|
|
535
|
+
const stack = [...graph.get(root) ?? []];
|
|
536
|
+
let node;
|
|
537
|
+
while ((node = stack.pop()) !== void 0) {
|
|
538
|
+
if (visited.has(node) || node === root) continue;
|
|
539
|
+
visited.add(node);
|
|
540
|
+
if (deadSet.has(node)) {
|
|
541
|
+
chain.push(node);
|
|
542
|
+
for (const next of graph.get(node) ?? []) {
|
|
543
|
+
if (!visited.has(next)) stack.push(next);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return chain;
|
|
478
548
|
}
|
|
479
549
|
function findDeadExports(project, liveFiles) {
|
|
480
550
|
const importedNames = buildImportedNameMap(project, liveFiles);
|
|
@@ -507,51 +577,61 @@ function getFileSize(filePath) {
|
|
|
507
577
|
return 0;
|
|
508
578
|
}
|
|
509
579
|
}
|
|
510
|
-
function buildDeadCodeReport(
|
|
580
|
+
function buildDeadCodeReport(safeToDelete, transitivelyDead, chains, deadExports, rootDir) {
|
|
511
581
|
const rel = (p) => path5.relative(rootDir, p).replaceAll("\\", "/");
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
return sum + getFileSize(f) + chain.reduce((s, c) => s + getFileSize(c), 0);
|
|
515
|
-
}, 0);
|
|
582
|
+
const allDeadFiles = [...safeToDelete, ...transitivelyDead];
|
|
583
|
+
const totalBytes = allDeadFiles.reduce((sum, f) => sum + getFileSize(f), 0);
|
|
516
584
|
const totalKb = (totalBytes / 1024).toFixed(1);
|
|
517
585
|
const lines = [
|
|
518
586
|
"========================================",
|
|
519
587
|
" DEAD CODE REPORT",
|
|
520
|
-
`
|
|
521
|
-
`
|
|
522
|
-
`
|
|
588
|
+
` Safe to delete : ${safeToDelete.length}`,
|
|
589
|
+
` Transitively dead : ${transitivelyDead.length}`,
|
|
590
|
+
` Dead exports : ${deadExports.length}`,
|
|
591
|
+
` Recoverable : ~${totalKb} KB`,
|
|
523
592
|
"========================================",
|
|
524
593
|
""
|
|
525
594
|
];
|
|
526
|
-
if (
|
|
527
|
-
lines.push(
|
|
528
|
-
|
|
595
|
+
if (safeToDelete.length > 0) {
|
|
596
|
+
lines.push(
|
|
597
|
+
"\u2500\u2500 SAFE TO DELETE \u2500\u2500",
|
|
598
|
+
"(These files are not imported by any other file in the codebase)",
|
|
599
|
+
""
|
|
600
|
+
);
|
|
601
|
+
for (const filePath of safeToDelete) {
|
|
529
602
|
const chain = chains.get(filePath) ?? [];
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
603
|
+
const sizeKb = (getFileSize(filePath) / 1024).toFixed(1);
|
|
604
|
+
lines.push(` ${rel(filePath)} (~${sizeKb} KB)`);
|
|
605
|
+
if (chain.length > 0) {
|
|
606
|
+
lines.push(` \u2514\u2500 also makes dead: ${chain.map(rel).join(", ")}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
lines.push("");
|
|
610
|
+
}
|
|
611
|
+
if (transitivelyDead.length > 0) {
|
|
612
|
+
lines.push(
|
|
613
|
+
"\u2500\u2500 TRANSITIVELY DEAD \u2500\u2500",
|
|
614
|
+
"(These files are only imported by dead files \u2014 they become orphaned too)",
|
|
615
|
+
""
|
|
616
|
+
);
|
|
617
|
+
for (const filePath of transitivelyDead) {
|
|
618
|
+
const sizeKb = (getFileSize(filePath) / 1024).toFixed(1);
|
|
619
|
+
lines.push(` ${rel(filePath)} (~${sizeKb} KB)`);
|
|
543
620
|
}
|
|
621
|
+
lines.push("");
|
|
544
622
|
}
|
|
545
623
|
if (deadExports.length > 0) {
|
|
546
|
-
lines.push(
|
|
624
|
+
lines.push(
|
|
625
|
+
"\u2500\u2500 DEAD EXPORTS \u2500\u2500",
|
|
626
|
+
"(Exported but never imported by any other file)",
|
|
627
|
+
""
|
|
628
|
+
);
|
|
547
629
|
for (const entry of deadExports) {
|
|
548
630
|
lines.push(
|
|
549
|
-
`
|
|
550
|
-
`Reason: Exported but never imported`,
|
|
551
|
-
`Action: Remove the export (file itself is still live)`,
|
|
552
|
-
""
|
|
631
|
+
` ${rel(entry.filePath)} \u2192 ${entry.exportName} [line ${entry.line}]`
|
|
553
632
|
);
|
|
554
633
|
}
|
|
634
|
+
lines.push("");
|
|
555
635
|
}
|
|
556
636
|
return lines.join("\n");
|
|
557
637
|
}
|
|
@@ -568,7 +648,8 @@ async function runDeadCode(dir, opts) {
|
|
|
568
648
|
const entries = findEntryPoints(dir, packageJson);
|
|
569
649
|
const result = runDeadCodeModule(project, graph, entries, dir);
|
|
570
650
|
const dead = [
|
|
571
|
-
...result.
|
|
651
|
+
...result.safeToDelete.map((f) => ({ file: f, exportName: "(entire file \u2014 safe to delete)" })),
|
|
652
|
+
...result.transitivelyDead.map((f) => ({ file: f, exportName: "(entire file \u2014 transitively dead)" })),
|
|
572
653
|
...result.deadExports.map((e) => ({ file: e.filePath, exportName: e.exportName }))
|
|
573
654
|
];
|
|
574
655
|
spinner.succeed(chalk.green(`Dead code scan complete \u2014 ${dead.length} item(s) found`));
|
|
@@ -644,7 +725,11 @@ function writeDeadOutput(result, opts) {
|
|
|
644
725
|
if (!opts.output) return;
|
|
645
726
|
if (opts.json) {
|
|
646
727
|
writeJson(
|
|
647
|
-
{
|
|
728
|
+
{
|
|
729
|
+
safeToDelete: result.safeToDelete,
|
|
730
|
+
transitivelyDead: result.transitivelyDead,
|
|
731
|
+
deadExports: result.deadExports
|
|
732
|
+
},
|
|
648
733
|
opts.output
|
|
649
734
|
);
|
|
650
735
|
console.log(chalk.cyan(` JSON written to ${opts.output}`));
|
|
@@ -653,16 +738,21 @@ function writeDeadOutput(result, opts) {
|
|
|
653
738
|
writeMarkdown(
|
|
654
739
|
{
|
|
655
740
|
title: "Dead Code Report",
|
|
656
|
-
summary: `${result.
|
|
741
|
+
summary: `${result.safeToDelete.length} safe to delete, ${result.transitivelyDead.length} transitively dead, ${result.deadExports.length} dead export(s)`,
|
|
657
742
|
sections: [
|
|
658
743
|
{
|
|
659
|
-
title: "
|
|
744
|
+
title: "Safe to Delete",
|
|
660
745
|
headers: ["File", "Chain"],
|
|
661
|
-
rows: result.
|
|
746
|
+
rows: result.safeToDelete.map((f) => [
|
|
662
747
|
f,
|
|
663
748
|
(result.chains.get(f) ?? []).join(" \u2192 ") || "\u2014"
|
|
664
749
|
])
|
|
665
750
|
},
|
|
751
|
+
{
|
|
752
|
+
title: "Transitively Dead",
|
|
753
|
+
headers: ["File"],
|
|
754
|
+
rows: result.transitivelyDead.map((f) => [f])
|
|
755
|
+
},
|
|
666
756
|
{
|
|
667
757
|
title: "Dead Exports",
|
|
668
758
|
headers: ["File", "Export", "Line"],
|
|
@@ -691,17 +781,30 @@ import {
|
|
|
691
781
|
} from "ts-morph";
|
|
692
782
|
|
|
693
783
|
// src/modules/dupe-finder.ts
|
|
784
|
+
var MIN_BLOCK_CHARS = 120;
|
|
694
785
|
async function runDupeFinder(dir, opts) {
|
|
695
|
-
const minLines = parseInt(opts.minLines ?? "
|
|
786
|
+
const minLines = parseInt(opts.minLines ?? "15", 10);
|
|
696
787
|
const spinner = ora3(chalk2.cyan(`Scanning for duplicate blocks (\u2265${minLines} lines)\u2026`)).start();
|
|
697
788
|
try {
|
|
698
|
-
const files = glob(dir, ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], [
|
|
789
|
+
const files = glob(dir, ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], [
|
|
790
|
+
"node_modules",
|
|
791
|
+
"dist",
|
|
792
|
+
".next",
|
|
793
|
+
"coverage",
|
|
794
|
+
"**/*.test.ts",
|
|
795
|
+
"**/*.test.tsx",
|
|
796
|
+
"**/*.spec.ts",
|
|
797
|
+
"**/*.spec.tsx",
|
|
798
|
+
"**/*.d.ts"
|
|
799
|
+
]);
|
|
699
800
|
const blockMap = /* @__PURE__ */ new Map();
|
|
700
801
|
for (const filePath of files) {
|
|
701
802
|
const content = fs6.readFileSync(filePath, "utf-8");
|
|
702
803
|
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
703
804
|
for (let i = 0; i <= lines.length - minLines; i++) {
|
|
704
805
|
const block = lines.slice(i, i + minLines).join("\n");
|
|
806
|
+
const meaningfulChars = block.replace(/[\s{}();,]/g, "").length;
|
|
807
|
+
if (meaningfulChars < MIN_BLOCK_CHARS) continue;
|
|
705
808
|
if (!blockMap.has(block)) blockMap.set(block, []);
|
|
706
809
|
blockMap.get(block).push({ file: filePath, startLine: i + 1 });
|
|
707
810
|
}
|
|
@@ -716,6 +819,8 @@ async function runDupeFinder(dir, opts) {
|
|
|
716
819
|
});
|
|
717
820
|
}
|
|
718
821
|
}
|
|
822
|
+
dupes.sort((a, b) => b.occurrences.length - a.occurrences.length);
|
|
823
|
+
if (dupes.length > 200) dupes.splice(200);
|
|
719
824
|
spinner.succeed(chalk2.green(`Duplicate scan complete \u2014 ${dupes.length} duplicate block(s) found`));
|
|
720
825
|
if (dupes.length === 0) {
|
|
721
826
|
console.log(chalk2.green(" No duplicate blocks detected."));
|
|
@@ -967,26 +1072,218 @@ async function runHealthReport(dir, opts) {
|
|
|
967
1072
|
);
|
|
968
1073
|
}
|
|
969
1074
|
|
|
1075
|
+
// src/modules/assets.ts
|
|
1076
|
+
import fs8 from "fs";
|
|
1077
|
+
import path8 from "path";
|
|
1078
|
+
import ora6 from "ora";
|
|
1079
|
+
import chalk6 from "chalk";
|
|
1080
|
+
import Table5 from "cli-table3";
|
|
1081
|
+
var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1082
|
+
".png",
|
|
1083
|
+
".jpg",
|
|
1084
|
+
".jpeg",
|
|
1085
|
+
".gif",
|
|
1086
|
+
".svg",
|
|
1087
|
+
".webp",
|
|
1088
|
+
".avif",
|
|
1089
|
+
".ico",
|
|
1090
|
+
".bmp",
|
|
1091
|
+
".woff",
|
|
1092
|
+
".woff2",
|
|
1093
|
+
".ttf",
|
|
1094
|
+
".eot",
|
|
1095
|
+
".otf",
|
|
1096
|
+
".mp4",
|
|
1097
|
+
".webm",
|
|
1098
|
+
".ogg",
|
|
1099
|
+
".mp3",
|
|
1100
|
+
".wav",
|
|
1101
|
+
".pdf"
|
|
1102
|
+
]);
|
|
1103
|
+
var SOURCE_PATTERNS2 = [
|
|
1104
|
+
"**/*.ts",
|
|
1105
|
+
"**/*.tsx",
|
|
1106
|
+
"**/*.js",
|
|
1107
|
+
"**/*.jsx",
|
|
1108
|
+
"**/*.css",
|
|
1109
|
+
"**/*.scss",
|
|
1110
|
+
"**/*.sass",
|
|
1111
|
+
"**/*.less",
|
|
1112
|
+
"**/*.html",
|
|
1113
|
+
"**/*.json"
|
|
1114
|
+
];
|
|
1115
|
+
var SOURCE_IGNORE = [
|
|
1116
|
+
"node_modules",
|
|
1117
|
+
"node_modules/**",
|
|
1118
|
+
"dist",
|
|
1119
|
+
"dist/**",
|
|
1120
|
+
".next",
|
|
1121
|
+
".next/**",
|
|
1122
|
+
"coverage",
|
|
1123
|
+
"coverage/**",
|
|
1124
|
+
"public",
|
|
1125
|
+
"public/**"
|
|
1126
|
+
];
|
|
1127
|
+
function runAssetCheckModule(rootDir) {
|
|
1128
|
+
const publicDir = path8.join(rootDir, "public");
|
|
1129
|
+
if (!fs8.existsSync(publicDir)) {
|
|
1130
|
+
return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
|
|
1131
|
+
}
|
|
1132
|
+
const assets = collectAssets(publicDir);
|
|
1133
|
+
if (assets.length === 0) {
|
|
1134
|
+
return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
|
|
1135
|
+
}
|
|
1136
|
+
const sourceFiles = glob(rootDir, SOURCE_PATTERNS2, SOURCE_IGNORE);
|
|
1137
|
+
const sourceContent = sourceFiles.reduce((acc, f) => {
|
|
1138
|
+
try {
|
|
1139
|
+
return acc + fs8.readFileSync(f, "utf-8") + "\n";
|
|
1140
|
+
} catch {
|
|
1141
|
+
return acc;
|
|
1142
|
+
}
|
|
1143
|
+
}, "");
|
|
1144
|
+
const unused = [];
|
|
1145
|
+
for (const assetAbs of assets) {
|
|
1146
|
+
const fileName = path8.basename(assetAbs);
|
|
1147
|
+
const relFromPublic = "/" + path8.relative(publicDir, assetAbs).replaceAll("\\", "/");
|
|
1148
|
+
const referenced = sourceContent.includes(fileName) || sourceContent.includes(relFromPublic);
|
|
1149
|
+
if (!referenced) {
|
|
1150
|
+
unused.push({
|
|
1151
|
+
filePath: assetAbs,
|
|
1152
|
+
relativePath: path8.relative(rootDir, assetAbs).replaceAll("\\", "/"),
|
|
1153
|
+
sizeBytes: getFileSize2(assetAbs)
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const report = buildAssetReport(unused, assets.length, rootDir);
|
|
1158
|
+
return { unusedAssets: unused, totalAssets: assets.length, report };
|
|
1159
|
+
}
|
|
1160
|
+
async function runAssetCheck(rootDir, opts) {
|
|
1161
|
+
const publicDir = path8.join(rootDir, "public");
|
|
1162
|
+
if (!fs8.existsSync(publicDir)) {
|
|
1163
|
+
console.log(chalk6.dim(" No public/ folder found \u2014 skipping asset check"));
|
|
1164
|
+
return [];
|
|
1165
|
+
}
|
|
1166
|
+
const spinner = ora6(chalk6.cyan("Scanning public/ for unused assets\u2026")).start();
|
|
1167
|
+
try {
|
|
1168
|
+
const result = runAssetCheckModule(rootDir);
|
|
1169
|
+
spinner.succeed(
|
|
1170
|
+
chalk6.green(
|
|
1171
|
+
`Asset scan complete \u2014 ${result.unusedAssets.length} unused / ${result.totalAssets} total`
|
|
1172
|
+
)
|
|
1173
|
+
);
|
|
1174
|
+
if (result.unusedAssets.length === 0) {
|
|
1175
|
+
console.log(chalk6.green(" All public assets are referenced in source."));
|
|
1176
|
+
return [];
|
|
1177
|
+
}
|
|
1178
|
+
const table = new Table5({ head: ["Asset", "Size"] });
|
|
1179
|
+
for (const asset of result.unusedAssets) {
|
|
1180
|
+
const kb = (asset.sizeBytes / 1024).toFixed(1);
|
|
1181
|
+
table.push([chalk6.gray(asset.relativePath), `${kb} KB`]);
|
|
1182
|
+
}
|
|
1183
|
+
console.log(table.toString());
|
|
1184
|
+
if (opts.output) {
|
|
1185
|
+
writeMarkdown(
|
|
1186
|
+
{
|
|
1187
|
+
title: "Unused Assets Report",
|
|
1188
|
+
summary: `${result.unusedAssets.length} unused asset(s) found in public/`,
|
|
1189
|
+
sections: [
|
|
1190
|
+
{
|
|
1191
|
+
title: "Unused Assets",
|
|
1192
|
+
headers: ["Asset", "Size (KB)"],
|
|
1193
|
+
rows: result.unusedAssets.map((a) => [
|
|
1194
|
+
a.relativePath,
|
|
1195
|
+
(a.sizeBytes / 1024).toFixed(1)
|
|
1196
|
+
])
|
|
1197
|
+
}
|
|
1198
|
+
],
|
|
1199
|
+
generatedAt: /* @__PURE__ */ new Date()
|
|
1200
|
+
},
|
|
1201
|
+
opts.output
|
|
1202
|
+
);
|
|
1203
|
+
console.log(chalk6.cyan(` Report written to ${opts.output}`));
|
|
1204
|
+
}
|
|
1205
|
+
return result.unusedAssets;
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
spinner.fail(chalk6.red("Asset scan failed"));
|
|
1208
|
+
throw err;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
function collectAssets(dir) {
|
|
1212
|
+
const results = [];
|
|
1213
|
+
function walk(current) {
|
|
1214
|
+
let entries;
|
|
1215
|
+
try {
|
|
1216
|
+
entries = fs8.readdirSync(current, { withFileTypes: true });
|
|
1217
|
+
} catch {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
for (const entry of entries) {
|
|
1221
|
+
const full = path8.join(current, entry.name);
|
|
1222
|
+
if (entry.isDirectory()) {
|
|
1223
|
+
walk(full);
|
|
1224
|
+
} else if (entry.isFile() && ASSET_EXTENSIONS.has(path8.extname(entry.name).toLowerCase())) {
|
|
1225
|
+
results.push(full);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
walk(dir);
|
|
1230
|
+
return results;
|
|
1231
|
+
}
|
|
1232
|
+
function getFileSize2(filePath) {
|
|
1233
|
+
try {
|
|
1234
|
+
return fs8.statSync(filePath).size;
|
|
1235
|
+
} catch {
|
|
1236
|
+
return 0;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function buildAssetReport(unused, totalAssets, rootDir) {
|
|
1240
|
+
const totalBytes = unused.reduce((s, a) => s + a.sizeBytes, 0);
|
|
1241
|
+
const totalKb = (totalBytes / 1024).toFixed(1);
|
|
1242
|
+
const lines = [
|
|
1243
|
+
"========================================",
|
|
1244
|
+
" UNUSED ASSETS REPORT",
|
|
1245
|
+
` Total assets : ${totalAssets}`,
|
|
1246
|
+
` Unused assets : ${unused.length}`,
|
|
1247
|
+
` Recoverable : ~${totalKb} KB`,
|
|
1248
|
+
"========================================",
|
|
1249
|
+
""
|
|
1250
|
+
];
|
|
1251
|
+
if (unused.length === 0) {
|
|
1252
|
+
lines.push(" All public assets are referenced in source.", "");
|
|
1253
|
+
return lines.join("\n");
|
|
1254
|
+
}
|
|
1255
|
+
lines.push("\u2500\u2500 UNUSED ASSETS \u2500\u2500", "");
|
|
1256
|
+
for (const asset of unused) {
|
|
1257
|
+
lines.push(
|
|
1258
|
+
`UNUSED \u2014 ${asset.relativePath}`,
|
|
1259
|
+
`Size: ~${(asset.sizeBytes / 1024).toFixed(1)} KB`,
|
|
1260
|
+
`Action: Safe to delete if not served directly via URL`,
|
|
1261
|
+
""
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
return lines.join("\n");
|
|
1265
|
+
}
|
|
1266
|
+
|
|
970
1267
|
// src/cli.ts
|
|
971
1268
|
function readPkgVersion() {
|
|
972
1269
|
try {
|
|
973
1270
|
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
974
|
-
const dir =
|
|
975
|
-
const pkgPath =
|
|
976
|
-
return JSON.parse(
|
|
1271
|
+
const dir = path9.dirname(fileURLToPath(import.meta.url));
|
|
1272
|
+
const pkgPath = path9.resolve(dir, "..", "package.json");
|
|
1273
|
+
return JSON.parse(fs9.readFileSync(pkgPath, "utf-8")).version;
|
|
977
1274
|
}
|
|
978
1275
|
} catch {
|
|
979
1276
|
}
|
|
980
1277
|
try {
|
|
981
1278
|
const dir = globalThis.__dirname ?? __dirname;
|
|
982
|
-
const pkgPath =
|
|
983
|
-
return JSON.parse(
|
|
1279
|
+
const pkgPath = path9.resolve(dir, "..", "package.json");
|
|
1280
|
+
return JSON.parse(fs9.readFileSync(pkgPath, "utf-8")).version;
|
|
984
1281
|
} catch {
|
|
985
1282
|
return "0.0.0";
|
|
986
1283
|
}
|
|
987
1284
|
}
|
|
988
1285
|
var PKG_VERSION = readPkgVersion();
|
|
989
|
-
var ALL_MODULES = ["dead-code", "dupes", "circular", "deps"];
|
|
1286
|
+
var ALL_MODULES = ["dead-code", "dupes", "circular", "deps", "assets"];
|
|
990
1287
|
var program = new Command();
|
|
991
1288
|
program.name("prunify").description("npm run clean. ship with confidence.").version(PKG_VERSION, "-v, --version").option("--dir <path>", "Root directory to analyze", process.cwd()).option("--entry <path>", "Override entry point").option("--only <modules>", "Comma-separated: dead-code,dupes,circular,deps,health").option(
|
|
992
1289
|
"--ignore <pattern>",
|
|
@@ -996,29 +1293,29 @@ program.name("prunify").description("npm run clean. ship with confidence.").vers
|
|
|
996
1293
|
).option("--out <path>", "Output directory for reports").option("--html", "Also generate code_health.html").option("--delete", "Prompt to delete dead files after analysis").option("--ci", "CI mode: exit 1 if issues found, no interactive prompts").action(main);
|
|
997
1294
|
program.parse();
|
|
998
1295
|
async function main(opts) {
|
|
999
|
-
const rootDir =
|
|
1000
|
-
if (!
|
|
1001
|
-
console.error(
|
|
1002
|
-
console.error(
|
|
1296
|
+
const rootDir = path9.resolve(opts.dir);
|
|
1297
|
+
if (!fs9.existsSync(path9.join(rootDir, "package.json"))) {
|
|
1298
|
+
console.error(chalk7.red(`\u2717 No package.json found in ${rootDir}`));
|
|
1299
|
+
console.error(chalk7.dim(" Use --dir <path> to point to your project root."));
|
|
1003
1300
|
process.exit(1);
|
|
1004
1301
|
}
|
|
1005
1302
|
const modules = resolveModules(opts.only);
|
|
1006
1303
|
console.log();
|
|
1007
|
-
console.log(
|
|
1304
|
+
console.log(chalk7.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
|
|
1008
1305
|
console.log();
|
|
1009
|
-
const parseSpinner = createSpinner(
|
|
1306
|
+
const parseSpinner = createSpinner(chalk7.cyan("Parsing codebase\u2026"));
|
|
1010
1307
|
const files = discoverFiles(rootDir, opts.ignore);
|
|
1011
|
-
parseSpinner.succeed(
|
|
1012
|
-
const graphSpinner = createSpinner(
|
|
1308
|
+
parseSpinner.succeed(chalk7.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
|
|
1309
|
+
const graphSpinner = createSpinner(chalk7.cyan("Building import graph\u2026"));
|
|
1013
1310
|
const project = buildProject(files);
|
|
1014
1311
|
const graph = buildGraph(files, (f) => {
|
|
1015
1312
|
const sf = project.getSourceFile(f);
|
|
1016
1313
|
return sf ? getImportsForFile(sf) : [];
|
|
1017
1314
|
});
|
|
1018
1315
|
const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0);
|
|
1019
|
-
graphSpinner.succeed(
|
|
1316
|
+
graphSpinner.succeed(chalk7.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
|
|
1020
1317
|
const packageJson = loadPackageJson2(rootDir);
|
|
1021
|
-
const entryPoints = opts.entry ? [
|
|
1318
|
+
const entryPoints = opts.entry ? [path9.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
|
|
1022
1319
|
const reportsDir = ensureReportsDir(rootDir, opts.out);
|
|
1023
1320
|
appendToGitignore(rootDir);
|
|
1024
1321
|
console.log();
|
|
@@ -1026,90 +1323,120 @@ async function main(opts) {
|
|
|
1026
1323
|
let dupeCount = 0;
|
|
1027
1324
|
let unusedPkgCount = 0;
|
|
1028
1325
|
let circularCount = 0;
|
|
1326
|
+
let unusedAssetCount = 0;
|
|
1029
1327
|
let deadReportFile = "";
|
|
1030
1328
|
let dupesReportFile = "";
|
|
1031
1329
|
let depsReportFile = "";
|
|
1032
1330
|
let circularReportFile = "";
|
|
1331
|
+
let assetsReportFile = "";
|
|
1033
1332
|
const deadFilePaths = [];
|
|
1034
1333
|
if (modules.includes("dead-code")) {
|
|
1035
|
-
const spinner = createSpinner(
|
|
1334
|
+
const spinner = createSpinner(chalk7.cyan("Analysing dead code\u2026"));
|
|
1036
1335
|
const result = runDeadCodeModule(project, graph, entryPoints, rootDir);
|
|
1037
|
-
deadFileCount = result.
|
|
1038
|
-
deadFilePaths.push(...result.
|
|
1039
|
-
spinner.succeed(
|
|
1336
|
+
deadFileCount = result.safeToDelete.length + result.transitivelyDead.length + result.deadExports.length;
|
|
1337
|
+
deadFilePaths.push(...result.safeToDelete, ...result.transitivelyDead);
|
|
1338
|
+
spinner.succeed(
|
|
1339
|
+
chalk7.green(
|
|
1340
|
+
`Dead code analysis complete \u2014 ${result.safeToDelete.length} safe to delete, ${result.transitivelyDead.length} transitively dead, ${result.deadExports.length} dead export(s)`
|
|
1341
|
+
)
|
|
1342
|
+
);
|
|
1040
1343
|
if (result.report) {
|
|
1041
1344
|
deadReportFile = "dead-code.txt";
|
|
1042
1345
|
writeReport(reportsDir, deadReportFile, result.report);
|
|
1043
1346
|
}
|
|
1044
1347
|
}
|
|
1045
1348
|
if (modules.includes("dupes")) {
|
|
1046
|
-
const outputPath =
|
|
1349
|
+
const outputPath = path9.join(reportsDir, "dupes.md");
|
|
1047
1350
|
const dupes = await runDupeFinder(rootDir, { output: outputPath });
|
|
1048
1351
|
dupeCount = dupes.length;
|
|
1049
1352
|
if (dupeCount > 0) dupesReportFile = "dupes.md";
|
|
1050
1353
|
}
|
|
1051
1354
|
if (modules.includes("circular")) {
|
|
1052
|
-
const spinner = createSpinner(
|
|
1355
|
+
const spinner = createSpinner(chalk7.cyan("Analysing circular imports\u2026"));
|
|
1053
1356
|
const cycles = detectCycles(graph);
|
|
1054
1357
|
circularCount = cycles.length;
|
|
1055
|
-
spinner.succeed(
|
|
1358
|
+
spinner.succeed(chalk7.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
|
|
1056
1359
|
if (circularCount > 0) {
|
|
1057
1360
|
circularReportFile = "circular.txt";
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1361
|
+
const rel = (p) => path9.relative(rootDir, p).replaceAll("\\", "/");
|
|
1362
|
+
const cycleLines = [
|
|
1363
|
+
"========================================",
|
|
1364
|
+
" CIRCULAR DEPENDENCIES",
|
|
1365
|
+
` Cycles found: ${cycles.length}`,
|
|
1366
|
+
"========================================",
|
|
1367
|
+
""
|
|
1368
|
+
];
|
|
1369
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
1370
|
+
const cycle = cycles[i];
|
|
1371
|
+
cycleLines.push(`Cycle ${i + 1} (${cycle.length} files):`);
|
|
1372
|
+
for (let j = 0; j < cycle.length; j++) {
|
|
1373
|
+
const arrow = j < cycle.length - 1 ? " \u2192 " : " \u2192 ";
|
|
1374
|
+
cycleLines.push(` ${rel(cycle[j])}`);
|
|
1375
|
+
}
|
|
1376
|
+
cycleLines.push(` \u21BB ${rel(cycle[0])} (back to start)`);
|
|
1377
|
+
cycleLines.push("");
|
|
1378
|
+
}
|
|
1379
|
+
writeReport(reportsDir, circularReportFile, cycleLines.join("\n"));
|
|
1060
1380
|
}
|
|
1061
1381
|
}
|
|
1062
1382
|
if (modules.includes("deps")) {
|
|
1063
|
-
const outputPath =
|
|
1383
|
+
const outputPath = path9.join(reportsDir, "deps.md");
|
|
1064
1384
|
const issues = await runDepCheck({ cwd: rootDir, output: outputPath });
|
|
1065
1385
|
unusedPkgCount = issues.filter((i) => i.type === "unused").length;
|
|
1066
1386
|
if (issues.length > 0) depsReportFile = "deps.md";
|
|
1067
1387
|
}
|
|
1388
|
+
if (modules.includes("assets")) {
|
|
1389
|
+
const outputPath = path9.join(reportsDir, "assets.md");
|
|
1390
|
+
const unusedAssets = await runAssetCheck(rootDir, { output: outputPath });
|
|
1391
|
+
unusedAssetCount = unusedAssets.length;
|
|
1392
|
+
if (unusedAssetCount > 0) assetsReportFile = "assets.md";
|
|
1393
|
+
}
|
|
1068
1394
|
if (modules.includes("health")) {
|
|
1069
|
-
const outputPath =
|
|
1395
|
+
const outputPath = path9.join(reportsDir, "health-report.md");
|
|
1070
1396
|
await runHealthReport(rootDir, { output: outputPath });
|
|
1071
1397
|
}
|
|
1072
1398
|
if (opts.html) {
|
|
1073
|
-
const htmlPath =
|
|
1399
|
+
const htmlPath = path9.join(reportsDir, "code_health.html");
|
|
1074
1400
|
writeHtmlReport(htmlPath, rootDir, deadFilePaths, circularCount, dupeCount, unusedPkgCount);
|
|
1075
|
-
console.log(
|
|
1401
|
+
console.log(chalk7.cyan(` HTML report written to ${htmlPath}`));
|
|
1076
1402
|
}
|
|
1077
1403
|
console.log();
|
|
1078
|
-
console.log(
|
|
1404
|
+
console.log(chalk7.bold("Summary"));
|
|
1079
1405
|
console.log();
|
|
1080
|
-
const table = new
|
|
1081
|
-
head: [
|
|
1406
|
+
const table = new Table6({
|
|
1407
|
+
head: [chalk7.bold("Check"), chalk7.bold("Found"), chalk7.bold("Output File")],
|
|
1082
1408
|
style: { head: [], border: [] }
|
|
1083
1409
|
});
|
|
1084
|
-
const fmt = (n) => n > 0 ?
|
|
1410
|
+
const fmt = (n) => n > 0 ? chalk7.yellow(String(n)) : chalk7.green("0");
|
|
1085
1411
|
table.push(
|
|
1086
|
-
["Dead
|
|
1412
|
+
["Dead Code (files + exports)", fmt(deadFileCount), deadReportFile || "\u2014"],
|
|
1413
|
+
["Circular Dependencies", fmt(circularCount), circularReportFile || "\u2014"],
|
|
1087
1414
|
["Duplicate Clusters", fmt(dupeCount), dupesReportFile || "\u2014"],
|
|
1088
1415
|
["Unused Packages", fmt(unusedPkgCount), depsReportFile || "\u2014"],
|
|
1089
|
-
["
|
|
1416
|
+
["Unused Assets", fmt(unusedAssetCount), assetsReportFile || "\u2014"]
|
|
1090
1417
|
);
|
|
1091
1418
|
console.log(table.toString());
|
|
1092
1419
|
console.log();
|
|
1093
1420
|
if (opts.delete && deadFilePaths.length > 0) {
|
|
1094
|
-
console.log(
|
|
1421
|
+
console.log(chalk7.yellow(`Dead files (${deadFilePaths.length}):`));
|
|
1095
1422
|
for (const f of deadFilePaths) {
|
|
1096
|
-
console.log(
|
|
1423
|
+
console.log(chalk7.dim(` ${path9.relative(rootDir, f)}`));
|
|
1097
1424
|
}
|
|
1098
1425
|
console.log();
|
|
1099
1426
|
if (!opts.ci) {
|
|
1100
1427
|
const confirmed = await confirmPrompt("Delete these files? (y/N) ");
|
|
1101
1428
|
if (confirmed) {
|
|
1102
1429
|
for (const f of deadFilePaths) {
|
|
1103
|
-
|
|
1430
|
+
fs9.rmSync(f, { force: true });
|
|
1104
1431
|
}
|
|
1105
|
-
console.log(
|
|
1432
|
+
console.log(chalk7.green(` Deleted ${deadFilePaths.length} file(s).`));
|
|
1106
1433
|
} else {
|
|
1107
|
-
console.log(
|
|
1434
|
+
console.log(chalk7.dim(" Skipped."));
|
|
1108
1435
|
}
|
|
1109
1436
|
}
|
|
1110
1437
|
}
|
|
1111
1438
|
if (opts.ci) {
|
|
1112
|
-
const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0;
|
|
1439
|
+
const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0;
|
|
1113
1440
|
if (hasIssues) process.exit(1);
|
|
1114
1441
|
}
|
|
1115
1442
|
}
|
|
@@ -1120,7 +1447,7 @@ function resolveModules(only) {
|
|
|
1120
1447
|
}
|
|
1121
1448
|
function loadPackageJson2(dir) {
|
|
1122
1449
|
try {
|
|
1123
|
-
return JSON.parse(
|
|
1450
|
+
return JSON.parse(fs9.readFileSync(path9.join(dir, "package.json"), "utf-8"));
|
|
1124
1451
|
} catch {
|
|
1125
1452
|
return null;
|
|
1126
1453
|
}
|
|
@@ -1141,7 +1468,7 @@ function writeHtmlReport(outputPath, rootDir, deadFiles, circularCount, dupeCoun
|
|
|
1141
1468
|
["Circular Dependencies", String(circularCount)],
|
|
1142
1469
|
["Unused Packages", String(unusedPkgCount)]
|
|
1143
1470
|
].map(([label, val]) => ` <tr><td>${label}</td><td>${val}</td></tr>`).join("\n");
|
|
1144
|
-
const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${
|
|
1471
|
+
const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${path9.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
|
|
1145
1472
|
const html = `<!DOCTYPE html>
|
|
1146
1473
|
<html lang="en">
|
|
1147
1474
|
<head>
|
|
@@ -1168,7 +1495,7 @@ ${rows}
|
|
|
1168
1495
|
${deadList}
|
|
1169
1496
|
</body>
|
|
1170
1497
|
</html>`;
|
|
1171
|
-
|
|
1172
|
-
|
|
1498
|
+
fs9.mkdirSync(path9.dirname(outputPath), { recursive: true });
|
|
1499
|
+
fs9.writeFileSync(outputPath, html, "utf-8");
|
|
1173
1500
|
}
|
|
1174
1501
|
//# sourceMappingURL=cli.js.map
|