prunify 0.1.2 → 0.1.5
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.cjs +209 -102
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +209 -102
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -209,40 +209,12 @@ function buildGraph(files, getImports) {
|
|
|
209
209
|
function findEntryPoints(rootDir, packageJson) {
|
|
210
210
|
const entries = [
|
|
211
211
|
...resolveNextJsEntries(rootDir),
|
|
212
|
+
...resolveFileBrowserEntries(rootDir),
|
|
212
213
|
...resolvePkgFieldEntries(rootDir, packageJson),
|
|
213
214
|
...resolveFallbackEntries(rootDir)
|
|
214
215
|
];
|
|
215
216
|
return [...new Set(entries)];
|
|
216
217
|
}
|
|
217
|
-
function findRootFiles(graph) {
|
|
218
|
-
const imported = /* @__PURE__ */ new Set();
|
|
219
|
-
for (const deps of graph.values()) {
|
|
220
|
-
for (const dep of deps) imported.add(dep);
|
|
221
|
-
}
|
|
222
|
-
const roots = [...graph.keys()].filter((f) => !imported.has(f));
|
|
223
|
-
return roots.length > 0 ? roots : [...graph.keys()];
|
|
224
|
-
}
|
|
225
|
-
function runDFS(graph, entryPoints) {
|
|
226
|
-
const visited = /* @__PURE__ */ new Set();
|
|
227
|
-
const stack = [...entryPoints];
|
|
228
|
-
let node;
|
|
229
|
-
while ((node = stack.pop()) !== void 0) {
|
|
230
|
-
if (visited.has(node)) continue;
|
|
231
|
-
visited.add(node);
|
|
232
|
-
for (const neighbor of graph.get(node) ?? []) {
|
|
233
|
-
if (!visited.has(neighbor)) stack.push(neighbor);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return visited;
|
|
237
|
-
}
|
|
238
|
-
function findDeadChains(graph, deadFiles) {
|
|
239
|
-
const reverseGraph = buildReverseGraph(graph);
|
|
240
|
-
const result = /* @__PURE__ */ new Map();
|
|
241
|
-
for (const deadRoot of deadFiles) {
|
|
242
|
-
result.set(deadRoot, dfsDeadChain(deadRoot, graph, deadFiles, reverseGraph));
|
|
243
|
-
}
|
|
244
|
-
return result;
|
|
245
|
-
}
|
|
246
218
|
function detectCycles(graph) {
|
|
247
219
|
const cycles = [];
|
|
248
220
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
@@ -261,12 +233,21 @@ function resolveNextJsEntries(rootDir) {
|
|
|
261
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"));
|
|
262
234
|
if (!isNext) return [];
|
|
263
235
|
const entries = [];
|
|
264
|
-
for (const dir of ["pages", "app"]) {
|
|
236
|
+
for (const dir of ["pages", "app", "src/pages", "src/app"]) {
|
|
265
237
|
const dirPath = path3.join(rootDir, dir);
|
|
266
238
|
if (fs3.existsSync(dirPath)) entries.push(...collectSourceFiles(dirPath));
|
|
267
239
|
}
|
|
268
240
|
return entries;
|
|
269
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
|
+
}
|
|
270
251
|
function resolvePkgFieldEntries(rootDir, packageJson) {
|
|
271
252
|
const entries = [];
|
|
272
253
|
for (const field of ["main", "module"]) {
|
|
@@ -343,27 +324,6 @@ function recordCycle(cycleStart, path10, acc) {
|
|
|
343
324
|
acc.cycles.push(cycle);
|
|
344
325
|
}
|
|
345
326
|
}
|
|
346
|
-
function dfsDeadChain(deadRoot, graph, deadFiles, reverseGraph) {
|
|
347
|
-
const chain = [];
|
|
348
|
-
const visited = /* @__PURE__ */ new Set();
|
|
349
|
-
const stack = [...graph.get(deadRoot) ?? []];
|
|
350
|
-
let node;
|
|
351
|
-
while ((node = stack.pop()) !== void 0) {
|
|
352
|
-
if (visited.has(node) || node === deadRoot) continue;
|
|
353
|
-
visited.add(node);
|
|
354
|
-
if (deadFiles.has(node) || isOnlyImportedByDead(node, deadFiles, reverseGraph)) {
|
|
355
|
-
chain.push(node);
|
|
356
|
-
for (const next of graph.get(node) ?? []) {
|
|
357
|
-
if (!visited.has(next)) stack.push(next);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
return chain;
|
|
362
|
-
}
|
|
363
|
-
function isOnlyImportedByDead(file, deadFiles, reverseGraph) {
|
|
364
|
-
const importers = reverseGraph.get(file) ?? /* @__PURE__ */ new Set();
|
|
365
|
-
return importers.size === 0 || [...importers].every((imp) => deadFiles.has(imp));
|
|
366
|
-
}
|
|
367
327
|
function buildReverseGraph(graph) {
|
|
368
328
|
const rev = /* @__PURE__ */ new Map();
|
|
369
329
|
for (const [file] of graph) {
|
|
@@ -490,16 +450,101 @@ function ensureDir(dir) {
|
|
|
490
450
|
}
|
|
491
451
|
|
|
492
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
|
+
}
|
|
493
480
|
function runDeadCodeModule(project, graph, entryPoints, rootDir) {
|
|
494
481
|
const allFiles = [...graph.keys()];
|
|
495
|
-
const
|
|
496
|
-
const
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
|
|
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)));
|
|
500
514
|
const deadExports = findDeadExports(project, liveFiles);
|
|
501
|
-
const report = buildDeadCodeReport(
|
|
502
|
-
|
|
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;
|
|
503
548
|
}
|
|
504
549
|
function findDeadExports(project, liveFiles) {
|
|
505
550
|
const importedNames = buildImportedNameMap(project, liveFiles);
|
|
@@ -532,51 +577,61 @@ function getFileSize(filePath) {
|
|
|
532
577
|
return 0;
|
|
533
578
|
}
|
|
534
579
|
}
|
|
535
|
-
function buildDeadCodeReport(
|
|
580
|
+
function buildDeadCodeReport(safeToDelete, transitivelyDead, chains, deadExports, rootDir) {
|
|
536
581
|
const rel = (p) => path5.relative(rootDir, p).replaceAll("\\", "/");
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
return sum + getFileSize(f) + chain.reduce((s, c) => s + getFileSize(c), 0);
|
|
540
|
-
}, 0);
|
|
582
|
+
const allDeadFiles = [...safeToDelete, ...transitivelyDead];
|
|
583
|
+
const totalBytes = allDeadFiles.reduce((sum, f) => sum + getFileSize(f), 0);
|
|
541
584
|
const totalKb = (totalBytes / 1024).toFixed(1);
|
|
542
585
|
const lines = [
|
|
543
586
|
"========================================",
|
|
544
587
|
" DEAD CODE REPORT",
|
|
545
|
-
`
|
|
546
|
-
`
|
|
547
|
-
`
|
|
588
|
+
` Safe to delete : ${safeToDelete.length}`,
|
|
589
|
+
` Transitively dead : ${transitivelyDead.length}`,
|
|
590
|
+
` Dead exports : ${deadExports.length}`,
|
|
591
|
+
` Recoverable : ~${totalKb} KB`,
|
|
548
592
|
"========================================",
|
|
549
593
|
""
|
|
550
594
|
];
|
|
551
|
-
if (
|
|
552
|
-
lines.push(
|
|
553
|
-
|
|
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) {
|
|
554
602
|
const chain = chains.get(filePath) ?? [];
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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)`);
|
|
568
620
|
}
|
|
621
|
+
lines.push("");
|
|
569
622
|
}
|
|
570
623
|
if (deadExports.length > 0) {
|
|
571
|
-
lines.push(
|
|
624
|
+
lines.push(
|
|
625
|
+
"\u2500\u2500 DEAD EXPORTS \u2500\u2500",
|
|
626
|
+
"(Exported but never imported by any other file)",
|
|
627
|
+
""
|
|
628
|
+
);
|
|
572
629
|
for (const entry of deadExports) {
|
|
573
630
|
lines.push(
|
|
574
|
-
`
|
|
575
|
-
`Reason: Exported but never imported`,
|
|
576
|
-
`Action: Remove the export (file itself is still live)`,
|
|
577
|
-
""
|
|
631
|
+
` ${rel(entry.filePath)} \u2192 ${entry.exportName} [line ${entry.line}]`
|
|
578
632
|
);
|
|
579
633
|
}
|
|
634
|
+
lines.push("");
|
|
580
635
|
}
|
|
581
636
|
return lines.join("\n");
|
|
582
637
|
}
|
|
@@ -593,7 +648,8 @@ async function runDeadCode(dir, opts) {
|
|
|
593
648
|
const entries = findEntryPoints(dir, packageJson);
|
|
594
649
|
const result = runDeadCodeModule(project, graph, entries, dir);
|
|
595
650
|
const dead = [
|
|
596
|
-
...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)" })),
|
|
597
653
|
...result.deadExports.map((e) => ({ file: e.filePath, exportName: e.exportName }))
|
|
598
654
|
];
|
|
599
655
|
spinner.succeed(chalk.green(`Dead code scan complete \u2014 ${dead.length} item(s) found`));
|
|
@@ -669,7 +725,11 @@ function writeDeadOutput(result, opts) {
|
|
|
669
725
|
if (!opts.output) return;
|
|
670
726
|
if (opts.json) {
|
|
671
727
|
writeJson(
|
|
672
|
-
{
|
|
728
|
+
{
|
|
729
|
+
safeToDelete: result.safeToDelete,
|
|
730
|
+
transitivelyDead: result.transitivelyDead,
|
|
731
|
+
deadExports: result.deadExports
|
|
732
|
+
},
|
|
673
733
|
opts.output
|
|
674
734
|
);
|
|
675
735
|
console.log(chalk.cyan(` JSON written to ${opts.output}`));
|
|
@@ -678,16 +738,21 @@ function writeDeadOutput(result, opts) {
|
|
|
678
738
|
writeMarkdown(
|
|
679
739
|
{
|
|
680
740
|
title: "Dead Code Report",
|
|
681
|
-
summary: `${result.
|
|
741
|
+
summary: `${result.safeToDelete.length} safe to delete, ${result.transitivelyDead.length} transitively dead, ${result.deadExports.length} dead export(s)`,
|
|
682
742
|
sections: [
|
|
683
743
|
{
|
|
684
|
-
title: "
|
|
744
|
+
title: "Safe to Delete",
|
|
685
745
|
headers: ["File", "Chain"],
|
|
686
|
-
rows: result.
|
|
746
|
+
rows: result.safeToDelete.map((f) => [
|
|
687
747
|
f,
|
|
688
748
|
(result.chains.get(f) ?? []).join(" \u2192 ") || "\u2014"
|
|
689
749
|
])
|
|
690
750
|
},
|
|
751
|
+
{
|
|
752
|
+
title: "Transitively Dead",
|
|
753
|
+
headers: ["File"],
|
|
754
|
+
rows: result.transitivelyDead.map((f) => [f])
|
|
755
|
+
},
|
|
691
756
|
{
|
|
692
757
|
title: "Dead Exports",
|
|
693
758
|
headers: ["File", "Export", "Line"],
|
|
@@ -716,17 +781,30 @@ import {
|
|
|
716
781
|
} from "ts-morph";
|
|
717
782
|
|
|
718
783
|
// src/modules/dupe-finder.ts
|
|
784
|
+
var MIN_BLOCK_CHARS = 120;
|
|
719
785
|
async function runDupeFinder(dir, opts) {
|
|
720
|
-
const minLines = parseInt(opts.minLines ?? "
|
|
786
|
+
const minLines = parseInt(opts.minLines ?? "15", 10);
|
|
721
787
|
const spinner = ora3(chalk2.cyan(`Scanning for duplicate blocks (\u2265${minLines} lines)\u2026`)).start();
|
|
722
788
|
try {
|
|
723
|
-
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
|
+
]);
|
|
724
800
|
const blockMap = /* @__PURE__ */ new Map();
|
|
725
801
|
for (const filePath of files) {
|
|
726
802
|
const content = fs6.readFileSync(filePath, "utf-8");
|
|
727
803
|
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
728
804
|
for (let i = 0; i <= lines.length - minLines; i++) {
|
|
729
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;
|
|
730
808
|
if (!blockMap.has(block)) blockMap.set(block, []);
|
|
731
809
|
blockMap.get(block).push({ file: filePath, startLine: i + 1 });
|
|
732
810
|
}
|
|
@@ -741,6 +819,8 @@ async function runDupeFinder(dir, opts) {
|
|
|
741
819
|
});
|
|
742
820
|
}
|
|
743
821
|
}
|
|
822
|
+
dupes.sort((a, b) => b.occurrences.length - a.occurrences.length);
|
|
823
|
+
if (dupes.length > 200) dupes.splice(200);
|
|
744
824
|
spinner.succeed(chalk2.green(`Duplicate scan complete \u2014 ${dupes.length} duplicate block(s) found`));
|
|
745
825
|
if (dupes.length === 0) {
|
|
746
826
|
console.log(chalk2.green(" No duplicate blocks detected."));
|
|
@@ -1253,12 +1333,19 @@ async function main(opts) {
|
|
|
1253
1333
|
if (modules.includes("dead-code")) {
|
|
1254
1334
|
const spinner = createSpinner(chalk7.cyan("Analysing dead code\u2026"));
|
|
1255
1335
|
const result = runDeadCodeModule(project, graph, entryPoints, rootDir);
|
|
1256
|
-
deadFileCount = result.
|
|
1257
|
-
deadFilePaths.push(...result.
|
|
1258
|
-
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
|
+
);
|
|
1259
1343
|
if (result.report) {
|
|
1260
1344
|
deadReportFile = "dead-code.txt";
|
|
1261
|
-
|
|
1345
|
+
const banner = `prunify ${PKG_VERSION} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1346
|
+
|
|
1347
|
+
`;
|
|
1348
|
+
writeReport(reportsDir, deadReportFile, banner + result.report);
|
|
1262
1349
|
}
|
|
1263
1350
|
}
|
|
1264
1351
|
if (modules.includes("dupes")) {
|
|
@@ -1274,8 +1361,28 @@ async function main(opts) {
|
|
|
1274
1361
|
spinner.succeed(chalk7.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
|
|
1275
1362
|
if (circularCount > 0) {
|
|
1276
1363
|
circularReportFile = "circular.txt";
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1364
|
+
const rel = (p) => path9.relative(rootDir, p).replaceAll("\\", "/");
|
|
1365
|
+
const cycleLines = [
|
|
1366
|
+
"========================================",
|
|
1367
|
+
" CIRCULAR DEPENDENCIES",
|
|
1368
|
+
` Cycles found: ${cycles.length}`,
|
|
1369
|
+
"========================================",
|
|
1370
|
+
""
|
|
1371
|
+
];
|
|
1372
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
1373
|
+
const cycle = cycles[i];
|
|
1374
|
+
cycleLines.push(`Cycle ${i + 1} (${cycle.length} files):`);
|
|
1375
|
+
for (let j = 0; j < cycle.length; j++) {
|
|
1376
|
+
const arrow = j < cycle.length - 1 ? " \u2192 " : " \u2192 ";
|
|
1377
|
+
cycleLines.push(` ${rel(cycle[j])}`);
|
|
1378
|
+
}
|
|
1379
|
+
cycleLines.push(` \u21BB ${rel(cycle[0])} (back to start)`);
|
|
1380
|
+
cycleLines.push("");
|
|
1381
|
+
}
|
|
1382
|
+
const banner = `prunify ${PKG_VERSION} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1383
|
+
|
|
1384
|
+
`;
|
|
1385
|
+
writeReport(reportsDir, circularReportFile, banner + cycleLines.join("\n"));
|
|
1279
1386
|
}
|
|
1280
1387
|
}
|
|
1281
1388
|
if (modules.includes("deps")) {
|
|
@@ -1308,10 +1415,10 @@ async function main(opts) {
|
|
|
1308
1415
|
});
|
|
1309
1416
|
const fmt = (n) => n > 0 ? chalk7.yellow(String(n)) : chalk7.green("0");
|
|
1310
1417
|
table.push(
|
|
1311
|
-
["Dead
|
|
1418
|
+
["Dead Code (files + exports)", fmt(deadFileCount), deadReportFile || "\u2014"],
|
|
1419
|
+
["Circular Dependencies", fmt(circularCount), circularReportFile || "\u2014"],
|
|
1312
1420
|
["Duplicate Clusters", fmt(dupeCount), dupesReportFile || "\u2014"],
|
|
1313
1421
|
["Unused Packages", fmt(unusedPkgCount), depsReportFile || "\u2014"],
|
|
1314
|
-
["Circular Deps", fmt(circularCount), circularReportFile || "\u2014"],
|
|
1315
1422
|
["Unused Assets", fmt(unusedAssetCount), assetsReportFile || "\u2014"]
|
|
1316
1423
|
);
|
|
1317
1424
|
console.log(table.toString());
|