prunify 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
- import chalk6 from "chalk";
6
- import Table5 from "cli-table3";
7
- import fs8 from "fs";
8
- import path8 from "path";
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,14 +209,19 @@ function buildGraph(files, getImports) {
202
209
  function findEntryPoints(rootDir, packageJson) {
203
210
  const entries = [
204
211
  ...resolveNextJsEntries(rootDir),
205
- ...resolvePkgFieldEntries(rootDir, packageJson)
212
+ ...resolvePkgFieldEntries(rootDir, packageJson),
213
+ ...resolveFallbackEntries(rootDir)
206
214
  ];
207
- if (entries.length === 0) {
208
- const fallback = resolveFallbackEntry(rootDir);
209
- if (fallback) entries.push(fallback);
210
- }
211
215
  return [...new Set(entries)];
212
216
  }
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
+ }
213
225
  function runDFS(graph, entryPoints) {
214
226
  const visited = /* @__PURE__ */ new Set();
215
227
  const stack = [...entryPoints];
@@ -236,11 +248,11 @@ function detectCycles(graph) {
236
248
  const seenKeys = /* @__PURE__ */ new Set();
237
249
  const visited = /* @__PURE__ */ new Set();
238
250
  const inStack = /* @__PURE__ */ new Set();
239
- const path9 = [];
251
+ const path10 = [];
240
252
  const acc = { seenKeys, cycles };
241
253
  for (const start of graph.keys()) {
242
254
  if (!visited.has(start)) {
243
- dfsForCycles(start, graph, visited, inStack, path9, acc);
255
+ dfsForCycles(start, graph, visited, inStack, path10, acc);
244
256
  }
245
257
  }
246
258
  return cycles;
@@ -265,18 +277,31 @@ function resolvePkgFieldEntries(rootDir, packageJson) {
265
277
  }
266
278
  return entries;
267
279
  }
268
- function resolveFallbackEntry(rootDir) {
269
- const fallbacks = ["src/main.ts", "src/main.tsx", "src/index.ts", "src/index.tsx"];
270
- for (const rel of fallbacks) {
271
- const abs = path3.join(rootDir, rel);
272
- if (fs3.existsSync(abs)) return abs;
273
- }
274
- return void 0;
280
+ function resolveFallbackEntries(rootDir) {
281
+ const candidates = [
282
+ "src/main.ts",
283
+ "src/main.tsx",
284
+ "src/main.js",
285
+ "src/main.jsx",
286
+ "src/index.ts",
287
+ "src/index.tsx",
288
+ "src/index.js",
289
+ "src/index.jsx",
290
+ "src/App.ts",
291
+ "src/App.tsx",
292
+ "src/App.js",
293
+ "src/App.jsx",
294
+ "index.ts",
295
+ "index.tsx",
296
+ "index.js",
297
+ "index.jsx"
298
+ ];
299
+ return candidates.map((rel) => path3.join(rootDir, rel)).filter((abs) => fs3.existsSync(abs));
275
300
  }
276
301
  function mkFrame(node, graph) {
277
302
  return { node, neighbors: (graph.get(node) ?? /* @__PURE__ */ new Set()).values(), entered: false };
278
303
  }
279
- function dfsForCycles(start, graph, visited, inStack, path9, acc) {
304
+ function dfsForCycles(start, graph, visited, inStack, path10, acc) {
280
305
  const stack = [mkFrame(start, graph)];
281
306
  while (stack.length > 0) {
282
307
  const frame = stack.at(-1);
@@ -288,30 +313,30 @@ function dfsForCycles(start, graph, visited, inStack, path9, acc) {
288
313
  }
289
314
  frame.entered = true;
290
315
  inStack.add(frame.node);
291
- path9.push(frame.node);
316
+ path10.push(frame.node);
292
317
  }
293
318
  const { done, value: neighbor } = frame.neighbors.next();
294
319
  if (done) {
295
320
  stack.pop();
296
- path9.pop();
321
+ path10.pop();
297
322
  inStack.delete(frame.node);
298
323
  visited.add(frame.node);
299
324
  } else {
300
- handleCycleNeighbor(neighbor, stack, path9, inStack, visited, acc, graph);
325
+ handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph);
301
326
  }
302
327
  }
303
328
  }
304
- function handleCycleNeighbor(neighbor, stack, path9, inStack, visited, acc, graph) {
329
+ function handleCycleNeighbor(neighbor, stack, path10, inStack, visited, acc, graph) {
305
330
  if (inStack.has(neighbor)) {
306
- recordCycle(neighbor, path9, acc);
331
+ recordCycle(neighbor, path10, acc);
307
332
  } else if (!visited.has(neighbor)) {
308
333
  stack.push(mkFrame(neighbor, graph));
309
334
  }
310
335
  }
311
- function recordCycle(cycleStart, path9, acc) {
312
- const idx = path9.indexOf(cycleStart);
336
+ function recordCycle(cycleStart, path10, acc) {
337
+ const idx = path10.indexOf(cycleStart);
313
338
  if (idx === -1) return;
314
- const cycle = normalizeCycle(path9.slice(idx));
339
+ const cycle = normalizeCycle(path10.slice(idx));
315
340
  const key = cycle.join("\0");
316
341
  if (!acc.seenKeys.has(key)) {
317
342
  acc.seenKeys.add(key);
@@ -467,7 +492,7 @@ function ensureDir(dir) {
467
492
  // src/modules/dead-code.ts
468
493
  function runDeadCodeModule(project, graph, entryPoints, rootDir) {
469
494
  const allFiles = [...graph.keys()];
470
- const effectiveEntries = entryPoints.length > 0 ? entryPoints : allFiles.slice(0, 1);
495
+ const effectiveEntries = entryPoints.length > 0 ? entryPoints : findRootFiles(graph);
471
496
  const liveFiles = runDFS(graph, effectiveEntries);
472
497
  const deadFiles = allFiles.filter((f) => !liveFiles.has(f));
473
498
  const deadSet = new Set(deadFiles);
@@ -967,26 +992,218 @@ async function runHealthReport(dir, opts) {
967
992
  );
968
993
  }
969
994
 
995
+ // src/modules/assets.ts
996
+ import fs8 from "fs";
997
+ import path8 from "path";
998
+ import ora6 from "ora";
999
+ import chalk6 from "chalk";
1000
+ import Table5 from "cli-table3";
1001
+ var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
1002
+ ".png",
1003
+ ".jpg",
1004
+ ".jpeg",
1005
+ ".gif",
1006
+ ".svg",
1007
+ ".webp",
1008
+ ".avif",
1009
+ ".ico",
1010
+ ".bmp",
1011
+ ".woff",
1012
+ ".woff2",
1013
+ ".ttf",
1014
+ ".eot",
1015
+ ".otf",
1016
+ ".mp4",
1017
+ ".webm",
1018
+ ".ogg",
1019
+ ".mp3",
1020
+ ".wav",
1021
+ ".pdf"
1022
+ ]);
1023
+ var SOURCE_PATTERNS2 = [
1024
+ "**/*.ts",
1025
+ "**/*.tsx",
1026
+ "**/*.js",
1027
+ "**/*.jsx",
1028
+ "**/*.css",
1029
+ "**/*.scss",
1030
+ "**/*.sass",
1031
+ "**/*.less",
1032
+ "**/*.html",
1033
+ "**/*.json"
1034
+ ];
1035
+ var SOURCE_IGNORE = [
1036
+ "node_modules",
1037
+ "node_modules/**",
1038
+ "dist",
1039
+ "dist/**",
1040
+ ".next",
1041
+ ".next/**",
1042
+ "coverage",
1043
+ "coverage/**",
1044
+ "public",
1045
+ "public/**"
1046
+ ];
1047
+ function runAssetCheckModule(rootDir) {
1048
+ const publicDir = path8.join(rootDir, "public");
1049
+ if (!fs8.existsSync(publicDir)) {
1050
+ return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
1051
+ }
1052
+ const assets = collectAssets(publicDir);
1053
+ if (assets.length === 0) {
1054
+ return { unusedAssets: [], totalAssets: 0, report: buildAssetReport([], 0, rootDir) };
1055
+ }
1056
+ const sourceFiles = glob(rootDir, SOURCE_PATTERNS2, SOURCE_IGNORE);
1057
+ const sourceContent = sourceFiles.reduce((acc, f) => {
1058
+ try {
1059
+ return acc + fs8.readFileSync(f, "utf-8") + "\n";
1060
+ } catch {
1061
+ return acc;
1062
+ }
1063
+ }, "");
1064
+ const unused = [];
1065
+ for (const assetAbs of assets) {
1066
+ const fileName = path8.basename(assetAbs);
1067
+ const relFromPublic = "/" + path8.relative(publicDir, assetAbs).replaceAll("\\", "/");
1068
+ const referenced = sourceContent.includes(fileName) || sourceContent.includes(relFromPublic);
1069
+ if (!referenced) {
1070
+ unused.push({
1071
+ filePath: assetAbs,
1072
+ relativePath: path8.relative(rootDir, assetAbs).replaceAll("\\", "/"),
1073
+ sizeBytes: getFileSize2(assetAbs)
1074
+ });
1075
+ }
1076
+ }
1077
+ const report = buildAssetReport(unused, assets.length, rootDir);
1078
+ return { unusedAssets: unused, totalAssets: assets.length, report };
1079
+ }
1080
+ async function runAssetCheck(rootDir, opts) {
1081
+ const publicDir = path8.join(rootDir, "public");
1082
+ if (!fs8.existsSync(publicDir)) {
1083
+ console.log(chalk6.dim(" No public/ folder found \u2014 skipping asset check"));
1084
+ return [];
1085
+ }
1086
+ const spinner = ora6(chalk6.cyan("Scanning public/ for unused assets\u2026")).start();
1087
+ try {
1088
+ const result = runAssetCheckModule(rootDir);
1089
+ spinner.succeed(
1090
+ chalk6.green(
1091
+ `Asset scan complete \u2014 ${result.unusedAssets.length} unused / ${result.totalAssets} total`
1092
+ )
1093
+ );
1094
+ if (result.unusedAssets.length === 0) {
1095
+ console.log(chalk6.green(" All public assets are referenced in source."));
1096
+ return [];
1097
+ }
1098
+ const table = new Table5({ head: ["Asset", "Size"] });
1099
+ for (const asset of result.unusedAssets) {
1100
+ const kb = (asset.sizeBytes / 1024).toFixed(1);
1101
+ table.push([chalk6.gray(asset.relativePath), `${kb} KB`]);
1102
+ }
1103
+ console.log(table.toString());
1104
+ if (opts.output) {
1105
+ writeMarkdown(
1106
+ {
1107
+ title: "Unused Assets Report",
1108
+ summary: `${result.unusedAssets.length} unused asset(s) found in public/`,
1109
+ sections: [
1110
+ {
1111
+ title: "Unused Assets",
1112
+ headers: ["Asset", "Size (KB)"],
1113
+ rows: result.unusedAssets.map((a) => [
1114
+ a.relativePath,
1115
+ (a.sizeBytes / 1024).toFixed(1)
1116
+ ])
1117
+ }
1118
+ ],
1119
+ generatedAt: /* @__PURE__ */ new Date()
1120
+ },
1121
+ opts.output
1122
+ );
1123
+ console.log(chalk6.cyan(` Report written to ${opts.output}`));
1124
+ }
1125
+ return result.unusedAssets;
1126
+ } catch (err) {
1127
+ spinner.fail(chalk6.red("Asset scan failed"));
1128
+ throw err;
1129
+ }
1130
+ }
1131
+ function collectAssets(dir) {
1132
+ const results = [];
1133
+ function walk(current) {
1134
+ let entries;
1135
+ try {
1136
+ entries = fs8.readdirSync(current, { withFileTypes: true });
1137
+ } catch {
1138
+ return;
1139
+ }
1140
+ for (const entry of entries) {
1141
+ const full = path8.join(current, entry.name);
1142
+ if (entry.isDirectory()) {
1143
+ walk(full);
1144
+ } else if (entry.isFile() && ASSET_EXTENSIONS.has(path8.extname(entry.name).toLowerCase())) {
1145
+ results.push(full);
1146
+ }
1147
+ }
1148
+ }
1149
+ walk(dir);
1150
+ return results;
1151
+ }
1152
+ function getFileSize2(filePath) {
1153
+ try {
1154
+ return fs8.statSync(filePath).size;
1155
+ } catch {
1156
+ return 0;
1157
+ }
1158
+ }
1159
+ function buildAssetReport(unused, totalAssets, rootDir) {
1160
+ const totalBytes = unused.reduce((s, a) => s + a.sizeBytes, 0);
1161
+ const totalKb = (totalBytes / 1024).toFixed(1);
1162
+ const lines = [
1163
+ "========================================",
1164
+ " UNUSED ASSETS REPORT",
1165
+ ` Total assets : ${totalAssets}`,
1166
+ ` Unused assets : ${unused.length}`,
1167
+ ` Recoverable : ~${totalKb} KB`,
1168
+ "========================================",
1169
+ ""
1170
+ ];
1171
+ if (unused.length === 0) {
1172
+ lines.push(" All public assets are referenced in source.", "");
1173
+ return lines.join("\n");
1174
+ }
1175
+ lines.push("\u2500\u2500 UNUSED ASSETS \u2500\u2500", "");
1176
+ for (const asset of unused) {
1177
+ lines.push(
1178
+ `UNUSED \u2014 ${asset.relativePath}`,
1179
+ `Size: ~${(asset.sizeBytes / 1024).toFixed(1)} KB`,
1180
+ `Action: Safe to delete if not served directly via URL`,
1181
+ ""
1182
+ );
1183
+ }
1184
+ return lines.join("\n");
1185
+ }
1186
+
970
1187
  // src/cli.ts
971
1188
  function readPkgVersion() {
972
1189
  try {
973
1190
  if (typeof import.meta !== "undefined" && import.meta.url) {
974
- const dir = path8.dirname(fileURLToPath(import.meta.url));
975
- const pkgPath = path8.resolve(dir, "..", "package.json");
976
- return JSON.parse(fs8.readFileSync(pkgPath, "utf-8")).version;
1191
+ const dir = path9.dirname(fileURLToPath(import.meta.url));
1192
+ const pkgPath = path9.resolve(dir, "..", "package.json");
1193
+ return JSON.parse(fs9.readFileSync(pkgPath, "utf-8")).version;
977
1194
  }
978
1195
  } catch {
979
1196
  }
980
1197
  try {
981
1198
  const dir = globalThis.__dirname ?? __dirname;
982
- const pkgPath = path8.resolve(dir, "..", "package.json");
983
- return JSON.parse(fs8.readFileSync(pkgPath, "utf-8")).version;
1199
+ const pkgPath = path9.resolve(dir, "..", "package.json");
1200
+ return JSON.parse(fs9.readFileSync(pkgPath, "utf-8")).version;
984
1201
  } catch {
985
1202
  return "0.0.0";
986
1203
  }
987
1204
  }
988
1205
  var PKG_VERSION = readPkgVersion();
989
- var ALL_MODULES = ["dead-code", "dupes", "circular", "deps"];
1206
+ var ALL_MODULES = ["dead-code", "dupes", "circular", "deps", "assets"];
990
1207
  var program = new Command();
991
1208
  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
1209
  "--ignore <pattern>",
@@ -996,29 +1213,29 @@ program.name("prunify").description("npm run clean. ship with confidence.").vers
996
1213
  ).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
1214
  program.parse();
998
1215
  async function main(opts) {
999
- const rootDir = path8.resolve(opts.dir);
1000
- if (!fs8.existsSync(path8.join(rootDir, "package.json"))) {
1001
- console.error(chalk6.red(`\u2717 No package.json found in ${rootDir}`));
1002
- console.error(chalk6.dim(" Use --dir <path> to point to your project root."));
1216
+ const rootDir = path9.resolve(opts.dir);
1217
+ if (!fs9.existsSync(path9.join(rootDir, "package.json"))) {
1218
+ console.error(chalk7.red(`\u2717 No package.json found in ${rootDir}`));
1219
+ console.error(chalk7.dim(" Use --dir <path> to point to your project root."));
1003
1220
  process.exit(1);
1004
1221
  }
1005
1222
  const modules = resolveModules(opts.only);
1006
1223
  console.log();
1007
- console.log(chalk6.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
1224
+ console.log(chalk7.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
1008
1225
  console.log();
1009
- const parseSpinner = createSpinner(chalk6.cyan("Parsing codebase\u2026"));
1226
+ const parseSpinner = createSpinner(chalk7.cyan("Parsing codebase\u2026"));
1010
1227
  const files = discoverFiles(rootDir, opts.ignore);
1011
- parseSpinner.succeed(chalk6.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
1012
- const graphSpinner = createSpinner(chalk6.cyan("Building import graph\u2026"));
1228
+ parseSpinner.succeed(chalk7.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
1229
+ const graphSpinner = createSpinner(chalk7.cyan("Building import graph\u2026"));
1013
1230
  const project = buildProject(files);
1014
1231
  const graph = buildGraph(files, (f) => {
1015
1232
  const sf = project.getSourceFile(f);
1016
1233
  return sf ? getImportsForFile(sf) : [];
1017
1234
  });
1018
1235
  const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0);
1019
- graphSpinner.succeed(chalk6.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
1236
+ graphSpinner.succeed(chalk7.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
1020
1237
  const packageJson = loadPackageJson2(rootDir);
1021
- const entryPoints = opts.entry ? [path8.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
1238
+ const entryPoints = opts.entry ? [path9.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
1022
1239
  const reportsDir = ensureReportsDir(rootDir, opts.out);
1023
1240
  appendToGitignore(rootDir);
1024
1241
  console.log();
@@ -1026,33 +1243,35 @@ async function main(opts) {
1026
1243
  let dupeCount = 0;
1027
1244
  let unusedPkgCount = 0;
1028
1245
  let circularCount = 0;
1246
+ let unusedAssetCount = 0;
1029
1247
  let deadReportFile = "";
1030
1248
  let dupesReportFile = "";
1031
1249
  let depsReportFile = "";
1032
1250
  let circularReportFile = "";
1251
+ let assetsReportFile = "";
1033
1252
  const deadFilePaths = [];
1034
1253
  if (modules.includes("dead-code")) {
1035
- const spinner = createSpinner(chalk6.cyan("Analysing dead code\u2026"));
1254
+ const spinner = createSpinner(chalk7.cyan("Analysing dead code\u2026"));
1036
1255
  const result = runDeadCodeModule(project, graph, entryPoints, rootDir);
1037
1256
  deadFileCount = result.deadFiles.length + result.deadExports.length;
1038
1257
  deadFilePaths.push(...result.deadFiles);
1039
- spinner.succeed(chalk6.green(`Dead code analysis complete \u2014 ${deadFileCount} item(s) found`));
1258
+ spinner.succeed(chalk7.green(`Dead code analysis complete \u2014 ${deadFileCount} item(s) found`));
1040
1259
  if (result.report) {
1041
1260
  deadReportFile = "dead-code.txt";
1042
1261
  writeReport(reportsDir, deadReportFile, result.report);
1043
1262
  }
1044
1263
  }
1045
1264
  if (modules.includes("dupes")) {
1046
- const outputPath = path8.join(reportsDir, "dupes.md");
1265
+ const outputPath = path9.join(reportsDir, "dupes.md");
1047
1266
  const dupes = await runDupeFinder(rootDir, { output: outputPath });
1048
1267
  dupeCount = dupes.length;
1049
1268
  if (dupeCount > 0) dupesReportFile = "dupes.md";
1050
1269
  }
1051
1270
  if (modules.includes("circular")) {
1052
- const spinner = createSpinner(chalk6.cyan("Analysing circular imports\u2026"));
1271
+ const spinner = createSpinner(chalk7.cyan("Analysing circular imports\u2026"));
1053
1272
  const cycles = detectCycles(graph);
1054
1273
  circularCount = cycles.length;
1055
- spinner.succeed(chalk6.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
1274
+ spinner.succeed(chalk7.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
1056
1275
  if (circularCount > 0) {
1057
1276
  circularReportFile = "circular.txt";
1058
1277
  const cycleText = cycles.map((c, i) => `Cycle ${i + 1}: ${c.join(" \u2192 ")}`).join("\n");
@@ -1060,56 +1279,63 @@ async function main(opts) {
1060
1279
  }
1061
1280
  }
1062
1281
  if (modules.includes("deps")) {
1063
- const outputPath = path8.join(reportsDir, "deps.md");
1282
+ const outputPath = path9.join(reportsDir, "deps.md");
1064
1283
  const issues = await runDepCheck({ cwd: rootDir, output: outputPath });
1065
1284
  unusedPkgCount = issues.filter((i) => i.type === "unused").length;
1066
1285
  if (issues.length > 0) depsReportFile = "deps.md";
1067
1286
  }
1287
+ if (modules.includes("assets")) {
1288
+ const outputPath = path9.join(reportsDir, "assets.md");
1289
+ const unusedAssets = await runAssetCheck(rootDir, { output: outputPath });
1290
+ unusedAssetCount = unusedAssets.length;
1291
+ if (unusedAssetCount > 0) assetsReportFile = "assets.md";
1292
+ }
1068
1293
  if (modules.includes("health")) {
1069
- const outputPath = path8.join(reportsDir, "health-report.md");
1294
+ const outputPath = path9.join(reportsDir, "health-report.md");
1070
1295
  await runHealthReport(rootDir, { output: outputPath });
1071
1296
  }
1072
1297
  if (opts.html) {
1073
- const htmlPath = path8.join(reportsDir, "code_health.html");
1298
+ const htmlPath = path9.join(reportsDir, "code_health.html");
1074
1299
  writeHtmlReport(htmlPath, rootDir, deadFilePaths, circularCount, dupeCount, unusedPkgCount);
1075
- console.log(chalk6.cyan(` HTML report written to ${htmlPath}`));
1300
+ console.log(chalk7.cyan(` HTML report written to ${htmlPath}`));
1076
1301
  }
1077
1302
  console.log();
1078
- console.log(chalk6.bold("Summary"));
1303
+ console.log(chalk7.bold("Summary"));
1079
1304
  console.log();
1080
- const table = new Table5({
1081
- head: [chalk6.bold("Check"), chalk6.bold("Found"), chalk6.bold("Output File")],
1305
+ const table = new Table6({
1306
+ head: [chalk7.bold("Check"), chalk7.bold("Found"), chalk7.bold("Output File")],
1082
1307
  style: { head: [], border: [] }
1083
1308
  });
1084
- const fmt = (n) => n > 0 ? chalk6.yellow(String(n)) : chalk6.green("0");
1309
+ const fmt = (n) => n > 0 ? chalk7.yellow(String(n)) : chalk7.green("0");
1085
1310
  table.push(
1086
1311
  ["Dead Files / Exports", fmt(deadFileCount), deadReportFile || "\u2014"],
1087
1312
  ["Duplicate Clusters", fmt(dupeCount), dupesReportFile || "\u2014"],
1088
1313
  ["Unused Packages", fmt(unusedPkgCount), depsReportFile || "\u2014"],
1089
- ["Circular Deps", fmt(circularCount), circularReportFile || "\u2014"]
1314
+ ["Circular Deps", fmt(circularCount), circularReportFile || "\u2014"],
1315
+ ["Unused Assets", fmt(unusedAssetCount), assetsReportFile || "\u2014"]
1090
1316
  );
1091
1317
  console.log(table.toString());
1092
1318
  console.log();
1093
1319
  if (opts.delete && deadFilePaths.length > 0) {
1094
- console.log(chalk6.yellow(`Dead files (${deadFilePaths.length}):`));
1320
+ console.log(chalk7.yellow(`Dead files (${deadFilePaths.length}):`));
1095
1321
  for (const f of deadFilePaths) {
1096
- console.log(chalk6.dim(` ${path8.relative(rootDir, f)}`));
1322
+ console.log(chalk7.dim(` ${path9.relative(rootDir, f)}`));
1097
1323
  }
1098
1324
  console.log();
1099
1325
  if (!opts.ci) {
1100
1326
  const confirmed = await confirmPrompt("Delete these files? (y/N) ");
1101
1327
  if (confirmed) {
1102
1328
  for (const f of deadFilePaths) {
1103
- fs8.rmSync(f, { force: true });
1329
+ fs9.rmSync(f, { force: true });
1104
1330
  }
1105
- console.log(chalk6.green(` Deleted ${deadFilePaths.length} file(s).`));
1331
+ console.log(chalk7.green(` Deleted ${deadFilePaths.length} file(s).`));
1106
1332
  } else {
1107
- console.log(chalk6.dim(" Skipped."));
1333
+ console.log(chalk7.dim(" Skipped."));
1108
1334
  }
1109
1335
  }
1110
1336
  }
1111
1337
  if (opts.ci) {
1112
- const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0;
1338
+ const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0;
1113
1339
  if (hasIssues) process.exit(1);
1114
1340
  }
1115
1341
  }
@@ -1120,7 +1346,7 @@ function resolveModules(only) {
1120
1346
  }
1121
1347
  function loadPackageJson2(dir) {
1122
1348
  try {
1123
- return JSON.parse(fs8.readFileSync(path8.join(dir, "package.json"), "utf-8"));
1349
+ return JSON.parse(fs9.readFileSync(path9.join(dir, "package.json"), "utf-8"));
1124
1350
  } catch {
1125
1351
  return null;
1126
1352
  }
@@ -1141,7 +1367,7 @@ function writeHtmlReport(outputPath, rootDir, deadFiles, circularCount, dupeCoun
1141
1367
  ["Circular Dependencies", String(circularCount)],
1142
1368
  ["Unused Packages", String(unusedPkgCount)]
1143
1369
  ].map(([label, val]) => ` <tr><td>${label}</td><td>${val}</td></tr>`).join("\n");
1144
- const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${path8.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
1370
+ const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${path9.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
1145
1371
  const html = `<!DOCTYPE html>
1146
1372
  <html lang="en">
1147
1373
  <head>
@@ -1168,7 +1394,7 @@ ${rows}
1168
1394
  ${deadList}
1169
1395
  </body>
1170
1396
  </html>`;
1171
- fs8.mkdirSync(path8.dirname(outputPath), { recursive: true });
1172
- fs8.writeFileSync(outputPath, html, "utf-8");
1397
+ fs9.mkdirSync(path9.dirname(outputPath), { recursive: true });
1398
+ fs9.writeFileSync(outputPath, html, "utf-8");
1173
1399
  }
1174
1400
  //# sourceMappingURL=cli.js.map