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/dist/cli.cjs CHANGED
@@ -25,10 +25,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/cli.ts
27
27
  var import_commander = require("commander");
28
- var import_chalk6 = __toESM(require("chalk"), 1);
29
- var import_cli_table35 = __toESM(require("cli-table3"), 1);
30
- var import_node_fs8 = __toESM(require("fs"), 1);
31
- var import_node_path8 = __toESM(require("path"), 1);
28
+ var import_chalk7 = __toESM(require("chalk"), 1);
29
+ var import_cli_table36 = __toESM(require("cli-table3"), 1);
30
+ var import_node_fs9 = __toESM(require("fs"), 1);
31
+ var import_node_path9 = __toESM(require("path"), 1);
32
32
  var import_node_url = require("url");
33
33
  var import_node_readline = __toESM(require("readline"), 1);
34
34
 
@@ -80,7 +80,14 @@ var DEFAULT_IGNORE = [
80
80
  "coverage",
81
81
  "coverage/**",
82
82
  "**/*.test.ts",
83
+ "**/*.test.tsx",
84
+ "**/*.test.js",
85
+ "**/*.test.jsx",
83
86
  "**/*.spec.ts",
87
+ "**/*.spec.tsx",
88
+ "**/*.spec.js",
89
+ "**/*.spec.jsx",
90
+ "**/*.stories.ts",
84
91
  "**/*.stories.tsx",
85
92
  "**/*.d.ts"
86
93
  ];
@@ -225,45 +232,22 @@ function buildGraph(files, getImports) {
225
232
  function findEntryPoints(rootDir, packageJson) {
226
233
  const entries = [
227
234
  ...resolveNextJsEntries(rootDir),
228
- ...resolvePkgFieldEntries(rootDir, packageJson)
235
+ ...resolveFileBrowserEntries(rootDir),
236
+ ...resolvePkgFieldEntries(rootDir, packageJson),
237
+ ...resolveFallbackEntries(rootDir)
229
238
  ];
230
- if (entries.length === 0) {
231
- const fallback = resolveFallbackEntry(rootDir);
232
- if (fallback) entries.push(fallback);
233
- }
234
239
  return [...new Set(entries)];
235
240
  }
236
- function runDFS(graph, entryPoints) {
237
- const visited = /* @__PURE__ */ new Set();
238
- const stack = [...entryPoints];
239
- let node;
240
- while ((node = stack.pop()) !== void 0) {
241
- if (visited.has(node)) continue;
242
- visited.add(node);
243
- for (const neighbor of graph.get(node) ?? []) {
244
- if (!visited.has(neighbor)) stack.push(neighbor);
245
- }
246
- }
247
- return visited;
248
- }
249
- function findDeadChains(graph, deadFiles) {
250
- const reverseGraph = buildReverseGraph(graph);
251
- const result = /* @__PURE__ */ new Map();
252
- for (const deadRoot of deadFiles) {
253
- result.set(deadRoot, dfsDeadChain(deadRoot, graph, deadFiles, reverseGraph));
254
- }
255
- return result;
256
- }
257
241
  function detectCycles(graph) {
258
242
  const cycles = [];
259
243
  const seenKeys = /* @__PURE__ */ new Set();
260
244
  const visited = /* @__PURE__ */ new Set();
261
245
  const inStack = /* @__PURE__ */ new Set();
262
- const path9 = [];
246
+ const path10 = [];
263
247
  const acc = { seenKeys, cycles };
264
248
  for (const start of graph.keys()) {
265
249
  if (!visited.has(start)) {
266
- dfsForCycles(start, graph, visited, inStack, path9, acc);
250
+ dfsForCycles(start, graph, visited, inStack, path10, acc);
267
251
  }
268
252
  }
269
253
  return cycles;
@@ -272,12 +256,21 @@ function resolveNextJsEntries(rootDir) {
272
256
  const isNext = import_node_fs3.default.existsSync(import_node_path3.default.join(rootDir, "next.config.js")) || import_node_fs3.default.existsSync(import_node_path3.default.join(rootDir, "next.config.ts")) || import_node_fs3.default.existsSync(import_node_path3.default.join(rootDir, "next.config.mjs"));
273
257
  if (!isNext) return [];
274
258
  const entries = [];
275
- for (const dir of ["pages", "app"]) {
259
+ for (const dir of ["pages", "app", "src/pages", "src/app"]) {
276
260
  const dirPath = import_node_path3.default.join(rootDir, dir);
277
261
  if (import_node_fs3.default.existsSync(dirPath)) entries.push(...collectSourceFiles(dirPath));
278
262
  }
279
263
  return entries;
280
264
  }
265
+ function resolveFileBrowserEntries(rootDir) {
266
+ const PAGE_DIRS = ["src/pages", "src/routes", "src/views", "src/screens"];
267
+ const entries = [];
268
+ for (const rel of PAGE_DIRS) {
269
+ const dirPath = import_node_path3.default.join(rootDir, rel);
270
+ if (import_node_fs3.default.existsSync(dirPath)) entries.push(...collectSourceFiles(dirPath));
271
+ }
272
+ return entries;
273
+ }
281
274
  function resolvePkgFieldEntries(rootDir, packageJson) {
282
275
  const entries = [];
283
276
  for (const field of ["main", "module"]) {
@@ -288,18 +281,31 @@ function resolvePkgFieldEntries(rootDir, packageJson) {
288
281
  }
289
282
  return entries;
290
283
  }
291
- function resolveFallbackEntry(rootDir) {
292
- const fallbacks = ["src/main.ts", "src/main.tsx", "src/index.ts", "src/index.tsx"];
293
- for (const rel of fallbacks) {
294
- const abs = import_node_path3.default.join(rootDir, rel);
295
- if (import_node_fs3.default.existsSync(abs)) return abs;
296
- }
297
- return void 0;
284
+ function resolveFallbackEntries(rootDir) {
285
+ const candidates = [
286
+ "src/main.ts",
287
+ "src/main.tsx",
288
+ "src/main.js",
289
+ "src/main.jsx",
290
+ "src/index.ts",
291
+ "src/index.tsx",
292
+ "src/index.js",
293
+ "src/index.jsx",
294
+ "src/App.ts",
295
+ "src/App.tsx",
296
+ "src/App.js",
297
+ "src/App.jsx",
298
+ "index.ts",
299
+ "index.tsx",
300
+ "index.js",
301
+ "index.jsx"
302
+ ];
303
+ return candidates.map((rel) => import_node_path3.default.join(rootDir, rel)).filter((abs) => import_node_fs3.default.existsSync(abs));
298
304
  }
299
305
  function mkFrame(node, graph) {
300
306
  return { node, neighbors: (graph.get(node) ?? /* @__PURE__ */ new Set()).values(), entered: false };
301
307
  }
302
- function dfsForCycles(start, graph, visited, inStack, path9, acc) {
308
+ function dfsForCycles(start, graph, visited, inStack, path10, acc) {
303
309
  const stack = [mkFrame(start, graph)];
304
310
  while (stack.length > 0) {
305
311
  const frame = stack.at(-1);
@@ -311,57 +317,36 @@ function dfsForCycles(start, graph, visited, inStack, path9, acc) {
311
317
  }
312
318
  frame.entered = true;
313
319
  inStack.add(frame.node);
314
- path9.push(frame.node);
320
+ path10.push(frame.node);
315
321
  }
316
322
  const { done, value: neighbor } = frame.neighbors.next();
317
323
  if (done) {
318
324
  stack.pop();
319
- path9.pop();
325
+ path10.pop();
320
326
  inStack.delete(frame.node);
321
327
  visited.add(frame.node);
322
328
  } else {
323
- handleCycleNeighbor(neighbor, stack, path9, inStack, visited, acc, graph);
329
+ handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph);
324
330
  }
325
331
  }
326
332
  }
327
- function handleCycleNeighbor(neighbor, stack, path9, inStack, visited, acc, graph) {
333
+ function handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph) {
328
334
  if (inStack.has(neighbor)) {
329
- recordCycle(neighbor, path9, acc);
335
+ recordCycle(neighbor, path10, acc);
330
336
  } else if (!visited.has(neighbor)) {
331
337
  stack.push(mkFrame(neighbor, graph));
332
338
  }
333
339
  }
334
- function recordCycle(cycleStart, path9, acc) {
335
- const idx = path9.indexOf(cycleStart);
340
+ function recordCycle(cycleStart, path10, acc) {
341
+ const idx = path10.indexOf(cycleStart);
336
342
  if (idx === -1) return;
337
- const cycle = normalizeCycle(path9.slice(idx));
343
+ const cycle = normalizeCycle(path10.slice(idx));
338
344
  const key = cycle.join("\0");
339
345
  if (!acc.seenKeys.has(key)) {
340
346
  acc.seenKeys.add(key);
341
347
  acc.cycles.push(cycle);
342
348
  }
343
349
  }
344
- function dfsDeadChain(deadRoot, graph, deadFiles, reverseGraph) {
345
- const chain = [];
346
- const visited = /* @__PURE__ */ new Set();
347
- const stack = [...graph.get(deadRoot) ?? []];
348
- let node;
349
- while ((node = stack.pop()) !== void 0) {
350
- if (visited.has(node) || node === deadRoot) continue;
351
- visited.add(node);
352
- if (deadFiles.has(node) || isOnlyImportedByDead(node, deadFiles, reverseGraph)) {
353
- chain.push(node);
354
- for (const next of graph.get(node) ?? []) {
355
- if (!visited.has(next)) stack.push(next);
356
- }
357
- }
358
- }
359
- return chain;
360
- }
361
- function isOnlyImportedByDead(file, deadFiles, reverseGraph) {
362
- const importers = reverseGraph.get(file) ?? /* @__PURE__ */ new Set();
363
- return importers.size === 0 || [...importers].every((imp) => deadFiles.has(imp));
364
- }
365
350
  function buildReverseGraph(graph) {
366
351
  const rev = /* @__PURE__ */ new Map();
367
352
  for (const [file] of graph) {
@@ -488,16 +473,101 @@ function ensureDir(dir) {
488
473
  }
489
474
 
490
475
  // src/modules/dead-code.ts
476
+ var FRAMEWORK_FILE_PATTERNS = [
477
+ /^next\.config\.(js|ts|mjs|cjs)$/,
478
+ /^middleware\.(ts|js)$/,
479
+ /^instrumentation\.(ts|js)$/,
480
+ /^tailwind\.config\.(js|ts|mjs|cjs)$/,
481
+ /^postcss\.config\.(js|ts|mjs|cjs)$/,
482
+ /^jest\.config\.(js|ts|mjs|cjs)$/,
483
+ /^vitest\.config\.(js|ts|mjs|cjs)$/,
484
+ /^vite\.config\.(js|ts|mjs|cjs)$/,
485
+ /^webpack\.config\.(js|ts|mjs|cjs)$/,
486
+ /^babel\.config\.(js|ts|cjs|mjs|json)$/,
487
+ /^\.babelrc\.(js|cjs)$/,
488
+ /^\.eslintrc\.(js|cjs)$/,
489
+ /^eslint\.config\.(js|ts|mjs|cjs)$/,
490
+ /^prettier\.config\.(js|ts|mjs|cjs)$/,
491
+ /^tsup\.config\.(ts|js)$/,
492
+ /^rollup\.config\.(js|ts|mjs)$/,
493
+ /^esbuild\.config\.(js|ts|mjs)$/,
494
+ /^commitlint\.config\.(js|ts)$/,
495
+ /^lint-staged\.config\.(js|ts|mjs|cjs)$/,
496
+ /^sentry\.(client|server|edge)\.config\.(ts|js)$/
497
+ ];
498
+ function isFrameworkFile(filePath, rootDir) {
499
+ const rel = import_node_path5.default.relative(rootDir, filePath);
500
+ const basename = import_node_path5.default.basename(rel);
501
+ return FRAMEWORK_FILE_PATTERNS.some((re) => re.test(basename));
502
+ }
491
503
  function runDeadCodeModule(project, graph, entryPoints, rootDir) {
492
504
  const allFiles = [...graph.keys()];
493
- const effectiveEntries = entryPoints.length > 0 ? entryPoints : allFiles.slice(0, 1);
494
- const liveFiles = runDFS(graph, effectiveEntries);
495
- const deadFiles = allFiles.filter((f) => !liveFiles.has(f));
496
- const deadSet = new Set(deadFiles);
497
- const chains = findDeadChains(graph, deadSet);
505
+ const entrySet = new Set(entryPoints);
506
+ const reverseGraph = buildReverseGraph(graph);
507
+ const excludedFiles = /* @__PURE__ */ new Set();
508
+ for (const file of allFiles) {
509
+ if (isFrameworkFile(file, rootDir)) excludedFiles.add(file);
510
+ }
511
+ const safeToDelete = allFiles.filter((f) => {
512
+ if (entrySet.has(f) || excludedFiles.has(f)) return false;
513
+ const importers = reverseGraph.get(f);
514
+ return !importers || importers.size === 0;
515
+ });
516
+ const deadSet = new Set(safeToDelete);
517
+ let changed = true;
518
+ while (changed) {
519
+ changed = false;
520
+ for (const file of allFiles) {
521
+ if (deadSet.has(file) || entrySet.has(file) || excludedFiles.has(file)) continue;
522
+ const importers = reverseGraph.get(file);
523
+ if (!importers || importers.size === 0) continue;
524
+ if ([...importers].every((imp) => deadSet.has(imp))) {
525
+ deadSet.add(file);
526
+ changed = true;
527
+ }
528
+ }
529
+ }
530
+ const transitivelyDead = [...deadSet].filter((f) => !safeToDelete.includes(f));
531
+ const chains = /* @__PURE__ */ new Map();
532
+ for (const root of safeToDelete) {
533
+ const chain = collectTransitiveChain(root, graph, deadSet);
534
+ chains.set(root, chain);
535
+ }
536
+ const liveFiles = new Set(allFiles.filter((f) => !deadSet.has(f)));
498
537
  const deadExports = findDeadExports(project, liveFiles);
499
- const report = buildDeadCodeReport(deadFiles, chains, deadExports, rootDir);
500
- return { deadFiles, liveFiles, chains, deadExports, report };
538
+ const report = buildDeadCodeReport(
539
+ safeToDelete,
540
+ transitivelyDead,
541
+ chains,
542
+ deadExports,
543
+ rootDir
544
+ );
545
+ return {
546
+ safeToDelete,
547
+ transitivelyDead,
548
+ deadFiles: [...deadSet],
549
+ liveFiles,
550
+ chains,
551
+ deadExports,
552
+ report
553
+ };
554
+ }
555
+ function collectTransitiveChain(root, graph, deadSet) {
556
+ const chain = [];
557
+ const visited = /* @__PURE__ */ new Set();
558
+ const stack = [...graph.get(root) ?? []];
559
+ let node;
560
+ while ((node = stack.pop()) !== void 0) {
561
+ if (visited.has(node) || node === root) continue;
562
+ visited.add(node);
563
+ if (deadSet.has(node)) {
564
+ chain.push(node);
565
+ for (const next of graph.get(node) ?? []) {
566
+ if (!visited.has(next)) stack.push(next);
567
+ }
568
+ }
569
+ }
570
+ return chain;
501
571
  }
502
572
  function findDeadExports(project, liveFiles) {
503
573
  const importedNames = buildImportedNameMap(project, liveFiles);
@@ -530,51 +600,61 @@ function getFileSize(filePath) {
530
600
  return 0;
531
601
  }
532
602
  }
533
- function buildDeadCodeReport(deadFiles, chains, deadExports, rootDir) {
603
+ function buildDeadCodeReport(safeToDelete, transitivelyDead, chains, deadExports, rootDir) {
534
604
  const rel = (p) => import_node_path5.default.relative(rootDir, p).replaceAll("\\", "/");
535
- const totalBytes = deadFiles.reduce((sum, f) => {
536
- const chain = chains.get(f) ?? [];
537
- return sum + getFileSize(f) + chain.reduce((s, c) => s + getFileSize(c), 0);
538
- }, 0);
605
+ const allDeadFiles = [...safeToDelete, ...transitivelyDead];
606
+ const totalBytes = allDeadFiles.reduce((sum, f) => sum + getFileSize(f), 0);
539
607
  const totalKb = (totalBytes / 1024).toFixed(1);
540
608
  const lines = [
541
609
  "========================================",
542
610
  " DEAD CODE REPORT",
543
- ` Dead files : ${deadFiles.length}`,
544
- ` Dead exports: ${deadExports.length}`,
545
- ` Recoverable : ~${totalKb} KB`,
611
+ ` Safe to delete : ${safeToDelete.length}`,
612
+ ` Transitively dead : ${transitivelyDead.length}`,
613
+ ` Dead exports : ${deadExports.length}`,
614
+ ` Recoverable : ~${totalKb} KB`,
546
615
  "========================================",
547
616
  ""
548
617
  ];
549
- if (deadFiles.length > 0) {
550
- lines.push("\u2500\u2500 DEAD FILES \u2500\u2500", "");
551
- for (const filePath of deadFiles) {
618
+ if (safeToDelete.length > 0) {
619
+ lines.push(
620
+ "\u2500\u2500 SAFE TO DELETE \u2500\u2500",
621
+ "(These files are not imported by any other file in the codebase)",
622
+ ""
623
+ );
624
+ for (const filePath of safeToDelete) {
552
625
  const chain = chains.get(filePath) ?? [];
553
- const allFiles = [filePath, ...chain];
554
- const sizeBytes = allFiles.reduce((s, f) => s + getFileSize(f), 0);
555
- const sizeKb = (sizeBytes / 1024).toFixed(1);
556
- const chainStr = chain.length > 0 ? [rel(filePath), ...chain.map(rel)].join(" \u2192 ") : rel(filePath);
557
- const plural = allFiles.length === 1 ? "" : "s";
558
- lines.push(
559
- `DEAD FILE \u2014 ${rel(filePath)}`,
560
- `Reason: Not imported anywhere in the codebase`,
561
- `Chain: ${chainStr}`,
562
- `Size: ~${sizeKb} KB removable across ${allFiles.length} file${plural}`,
563
- `Action: Safe to delete all ${allFiles.length} file${plural}`,
564
- ""
565
- );
626
+ const sizeKb = (getFileSize(filePath) / 1024).toFixed(1);
627
+ lines.push(` ${rel(filePath)} (~${sizeKb} KB)`);
628
+ if (chain.length > 0) {
629
+ lines.push(` \u2514\u2500 also makes dead: ${chain.map(rel).join(", ")}`);
630
+ }
631
+ }
632
+ lines.push("");
633
+ }
634
+ if (transitivelyDead.length > 0) {
635
+ lines.push(
636
+ "\u2500\u2500 TRANSITIVELY DEAD \u2500\u2500",
637
+ "(These files are only imported by dead files \u2014 they become orphaned too)",
638
+ ""
639
+ );
640
+ for (const filePath of transitivelyDead) {
641
+ const sizeKb = (getFileSize(filePath) / 1024).toFixed(1);
642
+ lines.push(` ${rel(filePath)} (~${sizeKb} KB)`);
566
643
  }
644
+ lines.push("");
567
645
  }
568
646
  if (deadExports.length > 0) {
569
- lines.push("\u2500\u2500 DEAD EXPORTS \u2500\u2500", "");
647
+ lines.push(
648
+ "\u2500\u2500 DEAD EXPORTS \u2500\u2500",
649
+ "(Exported but never imported by any other file)",
650
+ ""
651
+ );
570
652
  for (const entry of deadExports) {
571
653
  lines.push(
572
- `DEAD EXPORT \u2014 ${rel(entry.filePath)} \u2192 ${entry.exportName}() [line ${entry.line}]`,
573
- `Reason: Exported but never imported`,
574
- `Action: Remove the export (file itself is still live)`,
575
- ""
654
+ ` ${rel(entry.filePath)} \u2192 ${entry.exportName} [line ${entry.line}]`
576
655
  );
577
656
  }
657
+ lines.push("");
578
658
  }
579
659
  return lines.join("\n");
580
660
  }
@@ -591,7 +671,8 @@ async function runDeadCode(dir, opts) {
591
671
  const entries = findEntryPoints(dir, packageJson);
592
672
  const result = runDeadCodeModule(project, graph, entries, dir);
593
673
  const dead = [
594
- ...result.deadFiles.map((f) => ({ file: f, exportName: "(entire file)" })),
674
+ ...result.safeToDelete.map((f) => ({ file: f, exportName: "(entire file \u2014 safe to delete)" })),
675
+ ...result.transitivelyDead.map((f) => ({ file: f, exportName: "(entire file \u2014 transitively dead)" })),
595
676
  ...result.deadExports.map((e) => ({ file: e.filePath, exportName: e.exportName }))
596
677
  ];
597
678
  spinner.succeed(import_chalk.default.green(`Dead code scan complete \u2014 ${dead.length} item(s) found`));
@@ -667,7 +748,11 @@ function writeDeadOutput(result, opts) {
667
748
  if (!opts.output) return;
668
749
  if (opts.json) {
669
750
  writeJson(
670
- { deadFiles: result.deadFiles, deadExports: result.deadExports },
751
+ {
752
+ safeToDelete: result.safeToDelete,
753
+ transitivelyDead: result.transitivelyDead,
754
+ deadExports: result.deadExports
755
+ },
671
756
  opts.output
672
757
  );
673
758
  console.log(import_chalk.default.cyan(` JSON written to ${opts.output}`));
@@ -676,16 +761,21 @@ function writeDeadOutput(result, opts) {
676
761
  writeMarkdown(
677
762
  {
678
763
  title: "Dead Code Report",
679
- summary: `${result.deadFiles.length} dead file(s), ${result.deadExports.length} dead export(s)`,
764
+ summary: `${result.safeToDelete.length} safe to delete, ${result.transitivelyDead.length} transitively dead, ${result.deadExports.length} dead export(s)`,
680
765
  sections: [
681
766
  {
682
- title: "Dead Files",
767
+ title: "Safe to Delete",
683
768
  headers: ["File", "Chain"],
684
- rows: result.deadFiles.map((f) => [
769
+ rows: result.safeToDelete.map((f) => [
685
770
  f,
686
771
  (result.chains.get(f) ?? []).join(" \u2192 ") || "\u2014"
687
772
  ])
688
773
  },
774
+ {
775
+ title: "Transitively Dead",
776
+ headers: ["File"],
777
+ rows: result.transitivelyDead.map((f) => [f])
778
+ },
689
779
  {
690
780
  title: "Dead Exports",
691
781
  headers: ["File", "Export", "Line"],
@@ -711,17 +801,30 @@ var import_node_crypto = __toESM(require("crypto"), 1);
711
801
  var import_ts_morph3 = require("ts-morph");
712
802
 
713
803
  // src/modules/dupe-finder.ts
804
+ var MIN_BLOCK_CHARS = 120;
714
805
  async function runDupeFinder(dir, opts) {
715
- const minLines = parseInt(opts.minLines ?? "5", 10);
806
+ const minLines = parseInt(opts.minLines ?? "15", 10);
716
807
  const spinner = (0, import_ora3.default)(import_chalk2.default.cyan(`Scanning for duplicate blocks (\u2265${minLines} lines)\u2026`)).start();
717
808
  try {
718
- const files = glob(dir, ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], ["node_modules", "dist"]);
809
+ const files = glob(dir, ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], [
810
+ "node_modules",
811
+ "dist",
812
+ ".next",
813
+ "coverage",
814
+ "**/*.test.ts",
815
+ "**/*.test.tsx",
816
+ "**/*.spec.ts",
817
+ "**/*.spec.tsx",
818
+ "**/*.d.ts"
819
+ ]);
719
820
  const blockMap = /* @__PURE__ */ new Map();
720
821
  for (const filePath of files) {
721
822
  const content = import_node_fs6.default.readFileSync(filePath, "utf-8");
722
823
  const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
723
824
  for (let i = 0; i <= lines.length - minLines; i++) {
724
825
  const block = lines.slice(i, i + minLines).join("\n");
826
+ const meaningfulChars = block.replace(/[\s{}();,]/g, "").length;
827
+ if (meaningfulChars < MIN_BLOCK_CHARS) continue;
725
828
  if (!blockMap.has(block)) blockMap.set(block, []);
726
829
  blockMap.get(block).push({ file: filePath, startLine: i + 1 });
727
830
  }
@@ -736,6 +839,8 @@ async function runDupeFinder(dir, opts) {
736
839
  });
737
840
  }
738
841
  }
842
+ dupes.sort((a, b) => b.occurrences.length - a.occurrences.length);
843
+ if (dupes.length > 200) dupes.splice(200);
739
844
  spinner.succeed(import_chalk2.default.green(`Duplicate scan complete \u2014 ${dupes.length} duplicate block(s) found`));
740
845
  if (dupes.length === 0) {
741
846
  console.log(import_chalk2.default.green(" No duplicate blocks detected."));
@@ -987,27 +1092,219 @@ async function runHealthReport(dir, opts) {
987
1092
  );
988
1093
  }
989
1094
 
1095
+ // src/modules/assets.ts
1096
+ var import_node_fs8 = __toESM(require("fs"), 1);
1097
+ var import_node_path8 = __toESM(require("path"), 1);
1098
+ var import_ora6 = __toESM(require("ora"), 1);
1099
+ var import_chalk6 = __toESM(require("chalk"), 1);
1100
+ var import_cli_table35 = __toESM(require("cli-table3"), 1);
1101
+ var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
1102
+ ".png",
1103
+ ".jpg",
1104
+ ".jpeg",
1105
+ ".gif",
1106
+ ".svg",
1107
+ ".webp",
1108
+ ".avif",
1109
+ ".ico",
1110
+ ".bmp",
1111
+ ".woff",
1112
+ ".woff2",
1113
+ ".ttf",
1114
+ ".eot",
1115
+ ".otf",
1116
+ ".mp4",
1117
+ ".webm",
1118
+ ".ogg",
1119
+ ".mp3",
1120
+ ".wav",
1121
+ ".pdf"
1122
+ ]);
1123
+ var SOURCE_PATTERNS2 = [
1124
+ "**/*.ts",
1125
+ "**/*.tsx",
1126
+ "**/*.js",
1127
+ "**/*.jsx",
1128
+ "**/*.css",
1129
+ "**/*.scss",
1130
+ "**/*.sass",
1131
+ "**/*.less",
1132
+ "**/*.html",
1133
+ "**/*.json"
1134
+ ];
1135
+ var SOURCE_IGNORE = [
1136
+ "node_modules",
1137
+ "node_modules/**",
1138
+ "dist",
1139
+ "dist/**",
1140
+ ".next",
1141
+ ".next/**",
1142
+ "coverage",
1143
+ "coverage/**",
1144
+ "public",
1145
+ "public/**"
1146
+ ];
1147
+ function runAssetCheckModule(rootDir) {
1148
+ const publicDir = import_node_path8.default.join(rootDir, "public");
1149
+ if (!import_node_fs8.default.existsSync(publicDir)) {
1150
+ return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
1151
+ }
1152
+ const assets = collectAssets(publicDir);
1153
+ if (assets.length === 0) {
1154
+ return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
1155
+ }
1156
+ const sourceFiles = glob(rootDir, SOURCE_PATTERNS2, SOURCE_IGNORE);
1157
+ const sourceContent = sourceFiles.reduce((acc, f) => {
1158
+ try {
1159
+ return acc + import_node_fs8.default.readFileSync(f, "utf-8") + "\n";
1160
+ } catch {
1161
+ return acc;
1162
+ }
1163
+ }, "");
1164
+ const unused = [];
1165
+ for (const assetAbs of assets) {
1166
+ const fileName = import_node_path8.default.basename(assetAbs);
1167
+ const relFromPublic = "/" + import_node_path8.default.relative(publicDir, assetAbs).replaceAll("\\", "/");
1168
+ const referenced = sourceContent.includes(fileName) || sourceContent.includes(relFromPublic);
1169
+ if (!referenced) {
1170
+ unused.push({
1171
+ filePath: assetAbs,
1172
+ relativePath: import_node_path8.default.relative(rootDir, assetAbs).replaceAll("\\", "/"),
1173
+ sizeBytes: getFileSize2(assetAbs)
1174
+ });
1175
+ }
1176
+ }
1177
+ const report = buildAssetReport(unused, assets.length, rootDir);
1178
+ return { unusedAssets: unused, totalAssets: assets.length, report };
1179
+ }
1180
+ async function runAssetCheck(rootDir, opts) {
1181
+ const publicDir = import_node_path8.default.join(rootDir, "public");
1182
+ if (!import_node_fs8.default.existsSync(publicDir)) {
1183
+ console.log(import_chalk6.default.dim(" No public/ folder found \u2014 skipping asset check"));
1184
+ return [];
1185
+ }
1186
+ const spinner = (0, import_ora6.default)(import_chalk6.default.cyan("Scanning public/ for unused assets\u2026")).start();
1187
+ try {
1188
+ const result = runAssetCheckModule(rootDir);
1189
+ spinner.succeed(
1190
+ import_chalk6.default.green(
1191
+ `Asset scan complete \u2014 ${result.unusedAssets.length} unused / ${result.totalAssets} total`
1192
+ )
1193
+ );
1194
+ if (result.unusedAssets.length === 0) {
1195
+ console.log(import_chalk6.default.green(" All public assets are referenced in source."));
1196
+ return [];
1197
+ }
1198
+ const table = new import_cli_table35.default({ head: ["Asset", "Size"] });
1199
+ for (const asset of result.unusedAssets) {
1200
+ const kb = (asset.sizeBytes / 1024).toFixed(1);
1201
+ table.push([import_chalk6.default.gray(asset.relativePath), `${kb} KB`]);
1202
+ }
1203
+ console.log(table.toString());
1204
+ if (opts.output) {
1205
+ writeMarkdown(
1206
+ {
1207
+ title: "Unused Assets Report",
1208
+ summary: `${result.unusedAssets.length} unused asset(s) found in public/`,
1209
+ sections: [
1210
+ {
1211
+ title: "Unused Assets",
1212
+ headers: ["Asset", "Size (KB)"],
1213
+ rows: result.unusedAssets.map((a) => [
1214
+ a.relativePath,
1215
+ (a.sizeBytes / 1024).toFixed(1)
1216
+ ])
1217
+ }
1218
+ ],
1219
+ generatedAt: /* @__PURE__ */ new Date()
1220
+ },
1221
+ opts.output
1222
+ );
1223
+ console.log(import_chalk6.default.cyan(` Report written to ${opts.output}`));
1224
+ }
1225
+ return result.unusedAssets;
1226
+ } catch (err) {
1227
+ spinner.fail(import_chalk6.default.red("Asset scan failed"));
1228
+ throw err;
1229
+ }
1230
+ }
1231
+ function collectAssets(dir) {
1232
+ const results = [];
1233
+ function walk(current) {
1234
+ let entries;
1235
+ try {
1236
+ entries = import_node_fs8.default.readdirSync(current, { withFileTypes: true });
1237
+ } catch {
1238
+ return;
1239
+ }
1240
+ for (const entry of entries) {
1241
+ const full = import_node_path8.default.join(current, entry.name);
1242
+ if (entry.isDirectory()) {
1243
+ walk(full);
1244
+ } else if (entry.isFile() && ASSET_EXTENSIONS.has(import_node_path8.default.extname(entry.name).toLowerCase())) {
1245
+ results.push(full);
1246
+ }
1247
+ }
1248
+ }
1249
+ walk(dir);
1250
+ return results;
1251
+ }
1252
+ function getFileSize2(filePath) {
1253
+ try {
1254
+ return import_node_fs8.default.statSync(filePath).size;
1255
+ } catch {
1256
+ return 0;
1257
+ }
1258
+ }
1259
+ function buildAssetReport(unused, totalAssets, rootDir) {
1260
+ const totalBytes = unused.reduce((s, a) => s + a.sizeBytes, 0);
1261
+ const totalKb = (totalBytes / 1024).toFixed(1);
1262
+ const lines = [
1263
+ "========================================",
1264
+ " UNUSED ASSETS REPORT",
1265
+ ` Total assets : ${totalAssets}`,
1266
+ ` Unused assets : ${unused.length}`,
1267
+ ` Recoverable : ~${totalKb} KB`,
1268
+ "========================================",
1269
+ ""
1270
+ ];
1271
+ if (unused.length === 0) {
1272
+ lines.push(" All public assets are referenced in source.", "");
1273
+ return lines.join("\n");
1274
+ }
1275
+ lines.push("\u2500\u2500 UNUSED ASSETS \u2500\u2500", "");
1276
+ for (const asset of unused) {
1277
+ lines.push(
1278
+ `UNUSED \u2014 ${asset.relativePath}`,
1279
+ `Size: ~${(asset.sizeBytes / 1024).toFixed(1)} KB`,
1280
+ `Action: Safe to delete if not served directly via URL`,
1281
+ ""
1282
+ );
1283
+ }
1284
+ return lines.join("\n");
1285
+ }
1286
+
990
1287
  // src/cli.ts
991
1288
  var import_meta = {};
992
1289
  function readPkgVersion() {
993
1290
  try {
994
1291
  if (typeof import_meta !== "undefined" && import_meta.url) {
995
- const dir = import_node_path8.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
996
- const pkgPath = import_node_path8.default.resolve(dir, "..", "package.json");
997
- return JSON.parse(import_node_fs8.default.readFileSync(pkgPath, "utf-8")).version;
1292
+ const dir = import_node_path9.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
1293
+ const pkgPath = import_node_path9.default.resolve(dir, "..", "package.json");
1294
+ return JSON.parse(import_node_fs9.default.readFileSync(pkgPath, "utf-8")).version;
998
1295
  }
999
1296
  } catch {
1000
1297
  }
1001
1298
  try {
1002
1299
  const dir = globalThis.__dirname ?? __dirname;
1003
- const pkgPath = import_node_path8.default.resolve(dir, "..", "package.json");
1004
- return JSON.parse(import_node_fs8.default.readFileSync(pkgPath, "utf-8")).version;
1300
+ const pkgPath = import_node_path9.default.resolve(dir, "..", "package.json");
1301
+ return JSON.parse(import_node_fs9.default.readFileSync(pkgPath, "utf-8")).version;
1005
1302
  } catch {
1006
1303
  return "0.0.0";
1007
1304
  }
1008
1305
  }
1009
1306
  var PKG_VERSION = readPkgVersion();
1010
- var ALL_MODULES = ["dead-code", "dupes", "circular", "deps"];
1307
+ var ALL_MODULES = ["dead-code", "dupes", "circular", "deps", "assets"];
1011
1308
  var program = new import_commander.Command();
1012
1309
  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(
1013
1310
  "--ignore <pattern>",
@@ -1017,29 +1314,29 @@ program.name("prunify").description("npm run clean. ship with confidence.").vers
1017
1314
  ).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);
1018
1315
  program.parse();
1019
1316
  async function main(opts) {
1020
- const rootDir = import_node_path8.default.resolve(opts.dir);
1021
- if (!import_node_fs8.default.existsSync(import_node_path8.default.join(rootDir, "package.json"))) {
1022
- console.error(import_chalk6.default.red(`\u2717 No package.json found in ${rootDir}`));
1023
- console.error(import_chalk6.default.dim(" Use --dir <path> to point to your project root."));
1317
+ const rootDir = import_node_path9.default.resolve(opts.dir);
1318
+ if (!import_node_fs9.default.existsSync(import_node_path9.default.join(rootDir, "package.json"))) {
1319
+ console.error(import_chalk7.default.red(`\u2717 No package.json found in ${rootDir}`));
1320
+ console.error(import_chalk7.default.dim(" Use --dir <path> to point to your project root."));
1024
1321
  process.exit(1);
1025
1322
  }
1026
1323
  const modules = resolveModules(opts.only);
1027
1324
  console.log();
1028
- console.log(import_chalk6.default.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
1325
+ console.log(import_chalk7.default.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
1029
1326
  console.log();
1030
- const parseSpinner = createSpinner(import_chalk6.default.cyan("Parsing codebase\u2026"));
1327
+ const parseSpinner = createSpinner(import_chalk7.default.cyan("Parsing codebase\u2026"));
1031
1328
  const files = discoverFiles(rootDir, opts.ignore);
1032
- parseSpinner.succeed(import_chalk6.default.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
1033
- const graphSpinner = createSpinner(import_chalk6.default.cyan("Building import graph\u2026"));
1329
+ parseSpinner.succeed(import_chalk7.default.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
1330
+ const graphSpinner = createSpinner(import_chalk7.default.cyan("Building import graph\u2026"));
1034
1331
  const project = buildProject(files);
1035
1332
  const graph = buildGraph(files, (f) => {
1036
1333
  const sf = project.getSourceFile(f);
1037
1334
  return sf ? getImportsForFile(sf) : [];
1038
1335
  });
1039
1336
  const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0);
1040
- graphSpinner.succeed(import_chalk6.default.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
1337
+ graphSpinner.succeed(import_chalk7.default.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
1041
1338
  const packageJson = loadPackageJson2(rootDir);
1042
- const entryPoints = opts.entry ? [import_node_path8.default.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
1339
+ const entryPoints = opts.entry ? [import_node_path9.default.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
1043
1340
  const reportsDir = ensureReportsDir(rootDir, opts.out);
1044
1341
  appendToGitignore(rootDir);
1045
1342
  console.log();
@@ -1047,90 +1344,120 @@ async function main(opts) {
1047
1344
  let dupeCount = 0;
1048
1345
  let unusedPkgCount = 0;
1049
1346
  let circularCount = 0;
1347
+ let unusedAssetCount = 0;
1050
1348
  let deadReportFile = "";
1051
1349
  let dupesReportFile = "";
1052
1350
  let depsReportFile = "";
1053
1351
  let circularReportFile = "";
1352
+ let assetsReportFile = "";
1054
1353
  const deadFilePaths = [];
1055
1354
  if (modules.includes("dead-code")) {
1056
- const spinner = createSpinner(import_chalk6.default.cyan("Analysing dead code\u2026"));
1355
+ const spinner = createSpinner(import_chalk7.default.cyan("Analysing dead code\u2026"));
1057
1356
  const result = runDeadCodeModule(project, graph, entryPoints, rootDir);
1058
- deadFileCount = result.deadFiles.length + result.deadExports.length;
1059
- deadFilePaths.push(...result.deadFiles);
1060
- spinner.succeed(import_chalk6.default.green(`Dead code analysis complete \u2014 ${deadFileCount} item(s) found`));
1357
+ deadFileCount = result.safeToDelete.length + result.transitivelyDead.length + result.deadExports.length;
1358
+ deadFilePaths.push(...result.safeToDelete, ...result.transitivelyDead);
1359
+ spinner.succeed(
1360
+ import_chalk7.default.green(
1361
+ `Dead code analysis complete \u2014 ${result.safeToDelete.length} safe to delete, ${result.transitivelyDead.length} transitively dead, ${result.deadExports.length} dead export(s)`
1362
+ )
1363
+ );
1061
1364
  if (result.report) {
1062
1365
  deadReportFile = "dead-code.txt";
1063
1366
  writeReport(reportsDir, deadReportFile, result.report);
1064
1367
  }
1065
1368
  }
1066
1369
  if (modules.includes("dupes")) {
1067
- const outputPath = import_node_path8.default.join(reportsDir, "dupes.md");
1370
+ const outputPath = import_node_path9.default.join(reportsDir, "dupes.md");
1068
1371
  const dupes = await runDupeFinder(rootDir, { output: outputPath });
1069
1372
  dupeCount = dupes.length;
1070
1373
  if (dupeCount > 0) dupesReportFile = "dupes.md";
1071
1374
  }
1072
1375
  if (modules.includes("circular")) {
1073
- const spinner = createSpinner(import_chalk6.default.cyan("Analysing circular imports\u2026"));
1376
+ const spinner = createSpinner(import_chalk7.default.cyan("Analysing circular imports\u2026"));
1074
1377
  const cycles = detectCycles(graph);
1075
1378
  circularCount = cycles.length;
1076
- spinner.succeed(import_chalk6.default.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
1379
+ spinner.succeed(import_chalk7.default.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
1077
1380
  if (circularCount > 0) {
1078
1381
  circularReportFile = "circular.txt";
1079
- const cycleText = cycles.map((c, i) => `Cycle ${i + 1}: ${c.join(" \u2192 ")}`).join("\n");
1080
- writeReport(reportsDir, circularReportFile, cycleText);
1382
+ const rel = (p) => import_node_path9.default.relative(rootDir, p).replaceAll("\\", "/");
1383
+ const cycleLines = [
1384
+ "========================================",
1385
+ " CIRCULAR DEPENDENCIES",
1386
+ ` Cycles found: ${cycles.length}`,
1387
+ "========================================",
1388
+ ""
1389
+ ];
1390
+ for (let i = 0; i < cycles.length; i++) {
1391
+ const cycle = cycles[i];
1392
+ cycleLines.push(`Cycle ${i + 1} (${cycle.length} files):`);
1393
+ for (let j = 0; j < cycle.length; j++) {
1394
+ const arrow = j < cycle.length - 1 ? " \u2192 " : " \u2192 ";
1395
+ cycleLines.push(` ${rel(cycle[j])}`);
1396
+ }
1397
+ cycleLines.push(` \u21BB ${rel(cycle[0])} (back to start)`);
1398
+ cycleLines.push("");
1399
+ }
1400
+ writeReport(reportsDir, circularReportFile, cycleLines.join("\n"));
1081
1401
  }
1082
1402
  }
1083
1403
  if (modules.includes("deps")) {
1084
- const outputPath = import_node_path8.default.join(reportsDir, "deps.md");
1404
+ const outputPath = import_node_path9.default.join(reportsDir, "deps.md");
1085
1405
  const issues = await runDepCheck({ cwd: rootDir, output: outputPath });
1086
1406
  unusedPkgCount = issues.filter((i) => i.type === "unused").length;
1087
1407
  if (issues.length > 0) depsReportFile = "deps.md";
1088
1408
  }
1409
+ if (modules.includes("assets")) {
1410
+ const outputPath = import_node_path9.default.join(reportsDir, "assets.md");
1411
+ const unusedAssets = await runAssetCheck(rootDir, { output: outputPath });
1412
+ unusedAssetCount = unusedAssets.length;
1413
+ if (unusedAssetCount > 0) assetsReportFile = "assets.md";
1414
+ }
1089
1415
  if (modules.includes("health")) {
1090
- const outputPath = import_node_path8.default.join(reportsDir, "health-report.md");
1416
+ const outputPath = import_node_path9.default.join(reportsDir, "health-report.md");
1091
1417
  await runHealthReport(rootDir, { output: outputPath });
1092
1418
  }
1093
1419
  if (opts.html) {
1094
- const htmlPath = import_node_path8.default.join(reportsDir, "code_health.html");
1420
+ const htmlPath = import_node_path9.default.join(reportsDir, "code_health.html");
1095
1421
  writeHtmlReport(htmlPath, rootDir, deadFilePaths, circularCount, dupeCount, unusedPkgCount);
1096
- console.log(import_chalk6.default.cyan(` HTML report written to ${htmlPath}`));
1422
+ console.log(import_chalk7.default.cyan(` HTML report written to ${htmlPath}`));
1097
1423
  }
1098
1424
  console.log();
1099
- console.log(import_chalk6.default.bold("Summary"));
1425
+ console.log(import_chalk7.default.bold("Summary"));
1100
1426
  console.log();
1101
- const table = new import_cli_table35.default({
1102
- head: [import_chalk6.default.bold("Check"), import_chalk6.default.bold("Found"), import_chalk6.default.bold("Output File")],
1427
+ const table = new import_cli_table36.default({
1428
+ head: [import_chalk7.default.bold("Check"), import_chalk7.default.bold("Found"), import_chalk7.default.bold("Output File")],
1103
1429
  style: { head: [], border: [] }
1104
1430
  });
1105
- const fmt = (n) => n > 0 ? import_chalk6.default.yellow(String(n)) : import_chalk6.default.green("0");
1431
+ const fmt = (n) => n > 0 ? import_chalk7.default.yellow(String(n)) : import_chalk7.default.green("0");
1106
1432
  table.push(
1107
- ["Dead Files / Exports", fmt(deadFileCount), deadReportFile || "\u2014"],
1433
+ ["Dead Code (files + exports)", fmt(deadFileCount), deadReportFile || "\u2014"],
1434
+ ["Circular Dependencies", fmt(circularCount), circularReportFile || "\u2014"],
1108
1435
  ["Duplicate Clusters", fmt(dupeCount), dupesReportFile || "\u2014"],
1109
1436
  ["Unused Packages", fmt(unusedPkgCount), depsReportFile || "\u2014"],
1110
- ["Circular Deps", fmt(circularCount), circularReportFile || "\u2014"]
1437
+ ["Unused Assets", fmt(unusedAssetCount), assetsReportFile || "\u2014"]
1111
1438
  );
1112
1439
  console.log(table.toString());
1113
1440
  console.log();
1114
1441
  if (opts.delete && deadFilePaths.length > 0) {
1115
- console.log(import_chalk6.default.yellow(`Dead files (${deadFilePaths.length}):`));
1442
+ console.log(import_chalk7.default.yellow(`Dead files (${deadFilePaths.length}):`));
1116
1443
  for (const f of deadFilePaths) {
1117
- console.log(import_chalk6.default.dim(` ${import_node_path8.default.relative(rootDir, f)}`));
1444
+ console.log(import_chalk7.default.dim(` ${import_node_path9.default.relative(rootDir, f)}`));
1118
1445
  }
1119
1446
  console.log();
1120
1447
  if (!opts.ci) {
1121
1448
  const confirmed = await confirmPrompt("Delete these files? (y/N) ");
1122
1449
  if (confirmed) {
1123
1450
  for (const f of deadFilePaths) {
1124
- import_node_fs8.default.rmSync(f, { force: true });
1451
+ import_node_fs9.default.rmSync(f, { force: true });
1125
1452
  }
1126
- console.log(import_chalk6.default.green(` Deleted ${deadFilePaths.length} file(s).`));
1453
+ console.log(import_chalk7.default.green(` Deleted ${deadFilePaths.length} file(s).`));
1127
1454
  } else {
1128
- console.log(import_chalk6.default.dim(" Skipped."));
1455
+ console.log(import_chalk7.default.dim(" Skipped."));
1129
1456
  }
1130
1457
  }
1131
1458
  }
1132
1459
  if (opts.ci) {
1133
- const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0;
1460
+ const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0;
1134
1461
  if (hasIssues) process.exit(1);
1135
1462
  }
1136
1463
  }
@@ -1141,7 +1468,7 @@ function resolveModules(only) {
1141
1468
  }
1142
1469
  function loadPackageJson2(dir) {
1143
1470
  try {
1144
- return JSON.parse(import_node_fs8.default.readFileSync(import_node_path8.default.join(dir, "package.json"), "utf-8"));
1471
+ return JSON.parse(import_node_fs9.default.readFileSync(import_node_path9.default.join(dir, "package.json"), "utf-8"));
1145
1472
  } catch {
1146
1473
  return null;
1147
1474
  }
@@ -1162,7 +1489,7 @@ function writeHtmlReport(outputPath, rootDir, deadFiles, circularCount, dupeCoun
1162
1489
  ["Circular Dependencies", String(circularCount)],
1163
1490
  ["Unused Packages", String(unusedPkgCount)]
1164
1491
  ].map(([label, val]) => ` <tr><td>${label}</td><td>${val}</td></tr>`).join("\n");
1165
- const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${import_node_path8.default.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
1492
+ const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${import_node_path9.default.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
1166
1493
  const html = `<!DOCTYPE html>
1167
1494
  <html lang="en">
1168
1495
  <head>
@@ -1189,7 +1516,7 @@ ${rows}
1189
1516
  ${deadList}
1190
1517
  </body>
1191
1518
  </html>`;
1192
- import_node_fs8.default.mkdirSync(import_node_path8.default.dirname(outputPath), { recursive: true });
1193
- import_node_fs8.default.writeFileSync(outputPath, html, "utf-8");
1519
+ import_node_fs9.default.mkdirSync(import_node_path9.default.dirname(outputPath), { recursive: true });
1520
+ import_node_fs9.default.writeFileSync(outputPath, html, "utf-8");
1194
1521
  }
1195
1522
  //# sourceMappingURL=cli.cjs.map