prunify 0.1.4 → 0.1.6
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 +47 -25
- package/dist/cli.cjs +264 -56
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +264 -56
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import chalk7 from "chalk";
|
|
6
6
|
import Table6 from "cli-table3";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import fs10 from "fs";
|
|
8
|
+
import path10 from "path";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import readline from "readline";
|
|
11
11
|
|
|
@@ -220,11 +220,11 @@ function detectCycles(graph) {
|
|
|
220
220
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
221
221
|
const visited = /* @__PURE__ */ new Set();
|
|
222
222
|
const inStack = /* @__PURE__ */ new Set();
|
|
223
|
-
const
|
|
223
|
+
const path11 = [];
|
|
224
224
|
const acc = { seenKeys, cycles };
|
|
225
225
|
for (const start of graph.keys()) {
|
|
226
226
|
if (!visited.has(start)) {
|
|
227
|
-
dfsForCycles(start, graph, visited, inStack,
|
|
227
|
+
dfsForCycles(start, graph, visited, inStack, path11, acc);
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
return cycles;
|
|
@@ -282,7 +282,7 @@ function resolveFallbackEntries(rootDir) {
|
|
|
282
282
|
function mkFrame(node, graph) {
|
|
283
283
|
return { node, neighbors: (graph.get(node) ?? /* @__PURE__ */ new Set()).values(), entered: false };
|
|
284
284
|
}
|
|
285
|
-
function dfsForCycles(start, graph, visited, inStack,
|
|
285
|
+
function dfsForCycles(start, graph, visited, inStack, path11, acc) {
|
|
286
286
|
const stack = [mkFrame(start, graph)];
|
|
287
287
|
while (stack.length > 0) {
|
|
288
288
|
const frame = stack.at(-1);
|
|
@@ -294,30 +294,30 @@ function dfsForCycles(start, graph, visited, inStack, path10, acc) {
|
|
|
294
294
|
}
|
|
295
295
|
frame.entered = true;
|
|
296
296
|
inStack.add(frame.node);
|
|
297
|
-
|
|
297
|
+
path11.push(frame.node);
|
|
298
298
|
}
|
|
299
299
|
const { done, value: neighbor } = frame.neighbors.next();
|
|
300
300
|
if (done) {
|
|
301
301
|
stack.pop();
|
|
302
|
-
|
|
302
|
+
path11.pop();
|
|
303
303
|
inStack.delete(frame.node);
|
|
304
304
|
visited.add(frame.node);
|
|
305
305
|
} else {
|
|
306
|
-
handleCycleNeighbor(neighbor, stack,
|
|
306
|
+
handleCycleNeighbor(neighbor, stack, path11, inStack, visited, acc, graph);
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
|
-
function handleCycleNeighbor(neighbor, stack,
|
|
310
|
+
function handleCycleNeighbor(neighbor, stack, path11, inStack, visited, acc, graph) {
|
|
311
311
|
if (inStack.has(neighbor)) {
|
|
312
|
-
recordCycle(neighbor,
|
|
312
|
+
recordCycle(neighbor, path11, acc);
|
|
313
313
|
} else if (!visited.has(neighbor)) {
|
|
314
314
|
stack.push(mkFrame(neighbor, graph));
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
|
-
function recordCycle(cycleStart,
|
|
318
|
-
const idx =
|
|
317
|
+
function recordCycle(cycleStart, path11, acc) {
|
|
318
|
+
const idx = path11.indexOf(cycleStart);
|
|
319
319
|
if (idx === -1) return;
|
|
320
|
-
const cycle = normalizeCycle(
|
|
320
|
+
const cycle = normalizeCycle(path11.slice(idx));
|
|
321
321
|
const key = cycle.join("\0");
|
|
322
322
|
if (!acc.seenKeys.has(key)) {
|
|
323
323
|
acc.seenKeys.add(key);
|
|
@@ -1264,20 +1264,124 @@ function buildAssetReport(unused, totalAssets, rootDir) {
|
|
|
1264
1264
|
return lines.join("\n");
|
|
1265
1265
|
}
|
|
1266
1266
|
|
|
1267
|
+
// src/core/report-parser.ts
|
|
1268
|
+
import fs9 from "fs";
|
|
1269
|
+
import path9 from "path";
|
|
1270
|
+
function parseDeadCodeReportFile(reportPath, rootDir) {
|
|
1271
|
+
if (!fs9.existsSync(reportPath)) return [];
|
|
1272
|
+
const content = fs9.readFileSync(reportPath, "utf-8");
|
|
1273
|
+
return parseDeadCodeReportContent(content, rootDir);
|
|
1274
|
+
}
|
|
1275
|
+
function parseDeadCodeReportContent(content, rootDir) {
|
|
1276
|
+
const lines = content.split(/\r?\n/);
|
|
1277
|
+
const paths = [];
|
|
1278
|
+
const legacyRe = /^DEAD FILE —\s+(.+)$/;
|
|
1279
|
+
let legacyMode = content.includes("DEAD FILE \u2014") && content.includes("Dead files :");
|
|
1280
|
+
if (legacyMode) {
|
|
1281
|
+
for (const line of lines) {
|
|
1282
|
+
const m = legacyRe.exec(line.trim());
|
|
1283
|
+
if (m) paths.push(path9.resolve(rootDir, m[1].trim()));
|
|
1284
|
+
}
|
|
1285
|
+
return [...new Set(paths)];
|
|
1286
|
+
}
|
|
1287
|
+
let section = "none";
|
|
1288
|
+
const fileLineRe = /^\s{2}(.+?)\s+\(~[\d.]+\s+KB\)\s*$/;
|
|
1289
|
+
for (const line of lines) {
|
|
1290
|
+
if (line.includes("\u2500\u2500 SAFE TO DELETE \u2500\u2500")) {
|
|
1291
|
+
section = "safe";
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
if (line.includes("\u2500\u2500 TRANSITIVELY DEAD \u2500\u2500")) {
|
|
1295
|
+
section = "transitive";
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
if (line.includes("\u2500\u2500 DEAD EXPORTS \u2500\u2500")) {
|
|
1299
|
+
section = "none";
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
if (line.startsWith("\u2500\u2500 ") && line.includes("\u2500\u2500")) {
|
|
1303
|
+
if (section !== "none" && !line.includes("SAFE TO DELETE") && !line.includes("TRANSITIVELY DEAD")) {
|
|
1304
|
+
section = "none";
|
|
1305
|
+
}
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
if (section === "safe" || section === "transitive") {
|
|
1309
|
+
if (line.includes("\u2514\u2500") || line.includes("also makes dead")) continue;
|
|
1310
|
+
const m = fileLineRe.exec(line);
|
|
1311
|
+
if (m) {
|
|
1312
|
+
const rel = m[1].trim();
|
|
1313
|
+
if (rel && !rel.startsWith("(")) {
|
|
1314
|
+
paths.push(path9.resolve(rootDir, rel));
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return [...new Set(paths)];
|
|
1320
|
+
}
|
|
1321
|
+
function countDeadExportsInReport(content) {
|
|
1322
|
+
let section = false;
|
|
1323
|
+
let n = 0;
|
|
1324
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1325
|
+
if (line.includes("\u2500\u2500 DEAD EXPORTS \u2500\u2500")) {
|
|
1326
|
+
section = true;
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
if (section && line.startsWith("\u2500\u2500 ") && line.includes("\u2500\u2500")) break;
|
|
1330
|
+
if (section && /\s→\s.+\[line \d+\]/.test(line)) n++;
|
|
1331
|
+
}
|
|
1332
|
+
return n;
|
|
1333
|
+
}
|
|
1334
|
+
function parseAssetsReportFile(reportPath, rootDir) {
|
|
1335
|
+
if (!fs9.existsSync(reportPath)) return [];
|
|
1336
|
+
const content = fs9.readFileSync(reportPath, "utf-8");
|
|
1337
|
+
return parseAssetsReportContent(content, rootDir);
|
|
1338
|
+
}
|
|
1339
|
+
function parseAssetsReportContent(content, rootDir) {
|
|
1340
|
+
const lines = content.split(/\r?\n/);
|
|
1341
|
+
const paths = [];
|
|
1342
|
+
let inTable = false;
|
|
1343
|
+
let headerSeen = false;
|
|
1344
|
+
for (const line of lines) {
|
|
1345
|
+
const trimmed = line.trim();
|
|
1346
|
+
if (trimmed.startsWith("## Unused Assets")) {
|
|
1347
|
+
inTable = true;
|
|
1348
|
+
headerSeen = false;
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
if (inTable && trimmed.startsWith("## ")) {
|
|
1352
|
+
break;
|
|
1353
|
+
}
|
|
1354
|
+
if (!inTable) continue;
|
|
1355
|
+
if (trimmed.startsWith("|") && trimmed.includes("---")) {
|
|
1356
|
+
headerSeen = true;
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (!headerSeen || !trimmed.startsWith("|")) continue;
|
|
1360
|
+
const cells = trimmed.split("|").map((c) => c.trim()).filter(Boolean);
|
|
1361
|
+
if (cells.length >= 1) {
|
|
1362
|
+
const rel = cells[0];
|
|
1363
|
+
if (rel.toLowerCase() === "asset") continue;
|
|
1364
|
+
if (rel.includes("---")) continue;
|
|
1365
|
+
paths.push(path9.resolve(rootDir, rel));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return [...new Set(paths)];
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1267
1371
|
// src/cli.ts
|
|
1268
1372
|
function readPkgVersion() {
|
|
1269
1373
|
try {
|
|
1270
1374
|
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
1271
|
-
const dir =
|
|
1272
|
-
const pkgPath =
|
|
1273
|
-
return JSON.parse(
|
|
1375
|
+
const dir = path10.dirname(fileURLToPath(import.meta.url));
|
|
1376
|
+
const pkgPath = path10.resolve(dir, "..", "package.json");
|
|
1377
|
+
return JSON.parse(fs10.readFileSync(pkgPath, "utf-8")).version;
|
|
1274
1378
|
}
|
|
1275
1379
|
} catch {
|
|
1276
1380
|
}
|
|
1277
1381
|
try {
|
|
1278
1382
|
const dir = globalThis.__dirname ?? __dirname;
|
|
1279
|
-
const pkgPath =
|
|
1280
|
-
return JSON.parse(
|
|
1383
|
+
const pkgPath = path10.resolve(dir, "..", "package.json");
|
|
1384
|
+
return JSON.parse(fs10.readFileSync(pkgPath, "utf-8")).version;
|
|
1281
1385
|
} catch {
|
|
1282
1386
|
return "0.0.0";
|
|
1283
1387
|
}
|
|
@@ -1290,11 +1394,14 @@ program.name("prunify").description("npm run clean. ship with confidence.").vers
|
|
|
1290
1394
|
"Glob pattern to ignore (repeatable)",
|
|
1291
1395
|
(val, acc) => [...acc, val],
|
|
1292
1396
|
[]
|
|
1293
|
-
).option("--out <path>", "Output directory for reports").option("--html", "Also generate code_health.html").option("--delete", "
|
|
1397
|
+
).option("--out <path>", "Output directory for reports").option("--html", "Also generate code_health.html").option("--delete", "Delete dead files (uses prunify-reports/dead-code.txt if present, else runs analysis)").option(
|
|
1398
|
+
"--delete-assets",
|
|
1399
|
+
"Delete unused public assets (uses prunify-reports/assets.md if present, else runs asset scan)"
|
|
1400
|
+
).option("--ci", "CI mode: exit 1 if issues found, no interactive prompts").action(main);
|
|
1294
1401
|
program.parse();
|
|
1295
1402
|
async function main(opts) {
|
|
1296
|
-
const rootDir =
|
|
1297
|
-
if (!
|
|
1403
|
+
const rootDir = path10.resolve(opts.dir);
|
|
1404
|
+
if (!fs10.existsSync(path10.join(rootDir, "package.json"))) {
|
|
1298
1405
|
console.error(chalk7.red(`\u2717 No package.json found in ${rootDir}`));
|
|
1299
1406
|
console.error(chalk7.dim(" Use --dir <path> to point to your project root."));
|
|
1300
1407
|
process.exit(1);
|
|
@@ -1303,21 +1410,53 @@ async function main(opts) {
|
|
|
1303
1410
|
console.log();
|
|
1304
1411
|
console.log(chalk7.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
|
|
1305
1412
|
console.log();
|
|
1306
|
-
const parseSpinner = createSpinner(chalk7.cyan("Parsing codebase\u2026"));
|
|
1307
|
-
const files = discoverFiles(rootDir, opts.ignore);
|
|
1308
|
-
parseSpinner.succeed(chalk7.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
|
|
1309
|
-
const graphSpinner = createSpinner(chalk7.cyan("Building import graph\u2026"));
|
|
1310
|
-
const project = buildProject(files);
|
|
1311
|
-
const graph = buildGraph(files, (f) => {
|
|
1312
|
-
const sf = project.getSourceFile(f);
|
|
1313
|
-
return sf ? getImportsForFile(sf) : [];
|
|
1314
|
-
});
|
|
1315
|
-
const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0);
|
|
1316
|
-
graphSpinner.succeed(chalk7.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
|
|
1317
|
-
const packageJson = loadPackageJson2(rootDir);
|
|
1318
|
-
const entryPoints = opts.entry ? [path9.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
|
|
1319
1413
|
const reportsDir = ensureReportsDir(rootDir, opts.out);
|
|
1320
1414
|
appendToGitignore(rootDir);
|
|
1415
|
+
const deadReportPath = path10.join(reportsDir, "dead-code.txt");
|
|
1416
|
+
const assetsReportPath = path10.join(reportsDir, "assets.md");
|
|
1417
|
+
const loadDeadFromCache = Boolean(opts.delete && fs10.existsSync(deadReportPath));
|
|
1418
|
+
const loadAssetsFromCache = Boolean(opts.deleteAssets && fs10.existsSync(assetsReportPath));
|
|
1419
|
+
let deadFilePaths = [];
|
|
1420
|
+
let unusedAssetPaths = [];
|
|
1421
|
+
if (loadDeadFromCache) {
|
|
1422
|
+
deadFilePaths = parseDeadCodeReportFile(deadReportPath, rootDir);
|
|
1423
|
+
console.log(
|
|
1424
|
+
chalk7.cyan(
|
|
1425
|
+
` Using existing dead-code report (${deadFilePaths.length} file(s) to delete) \u2192 ${path10.relative(rootDir, deadReportPath)}`
|
|
1426
|
+
)
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
if (loadAssetsFromCache) {
|
|
1430
|
+
unusedAssetPaths = parseAssetsReportFile(assetsReportPath, rootDir);
|
|
1431
|
+
console.log(
|
|
1432
|
+
chalk7.cyan(
|
|
1433
|
+
` Using existing assets report (${unusedAssetPaths.length} asset(s) to delete) \u2192 ${path10.relative(rootDir, assetsReportPath)}`
|
|
1434
|
+
)
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
const runDeadCode2 = !loadDeadFromCache && (modules.includes("dead-code") || Boolean(opts.delete));
|
|
1438
|
+
const runCircular2 = modules.includes("circular");
|
|
1439
|
+
const runAssets = !loadAssetsFromCache && (modules.includes("assets") || Boolean(opts.deleteAssets));
|
|
1440
|
+
const needsGraph = runCircular2 || runDeadCode2;
|
|
1441
|
+
let files = [];
|
|
1442
|
+
let project;
|
|
1443
|
+
let graph;
|
|
1444
|
+
let entryPoints = [];
|
|
1445
|
+
if (needsGraph) {
|
|
1446
|
+
const parseSpinner = createSpinner(chalk7.cyan("Parsing codebase\u2026"));
|
|
1447
|
+
files = discoverFiles(rootDir, opts.ignore);
|
|
1448
|
+
parseSpinner.succeed(chalk7.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
|
|
1449
|
+
const graphSpinner = createSpinner(chalk7.cyan("Building import graph\u2026"));
|
|
1450
|
+
project = buildProject(files);
|
|
1451
|
+
graph = buildGraph(files, (f) => {
|
|
1452
|
+
const sf = project.getSourceFile(f);
|
|
1453
|
+
return sf ? getImportsForFile(sf) : [];
|
|
1454
|
+
});
|
|
1455
|
+
const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0);
|
|
1456
|
+
graphSpinner.succeed(chalk7.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
|
|
1457
|
+
const packageJson = loadPackageJson2(rootDir);
|
|
1458
|
+
entryPoints = opts.entry ? [path10.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
|
|
1459
|
+
}
|
|
1321
1460
|
console.log();
|
|
1322
1461
|
let deadFileCount = 0;
|
|
1323
1462
|
let dupeCount = 0;
|
|
@@ -1329,12 +1468,24 @@ async function main(opts) {
|
|
|
1329
1468
|
let depsReportFile = "";
|
|
1330
1469
|
let circularReportFile = "";
|
|
1331
1470
|
let assetsReportFile = "";
|
|
1332
|
-
|
|
1333
|
-
|
|
1471
|
+
if (loadDeadFromCache) {
|
|
1472
|
+
deadReportFile = "dead-code.txt";
|
|
1473
|
+
try {
|
|
1474
|
+
const raw = fs10.readFileSync(deadReportPath, "utf-8");
|
|
1475
|
+
deadFileCount = deadFilePaths.length + countDeadExportsInReport(raw);
|
|
1476
|
+
} catch {
|
|
1477
|
+
deadFileCount = deadFilePaths.length;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (loadAssetsFromCache) {
|
|
1481
|
+
assetsReportFile = "assets.md";
|
|
1482
|
+
unusedAssetCount = unusedAssetPaths.length;
|
|
1483
|
+
}
|
|
1484
|
+
if (runDeadCode2) {
|
|
1334
1485
|
const spinner = createSpinner(chalk7.cyan("Analysing dead code\u2026"));
|
|
1335
1486
|
const result = runDeadCodeModule(project, graph, entryPoints, rootDir);
|
|
1336
1487
|
deadFileCount = result.safeToDelete.length + result.transitivelyDead.length + result.deadExports.length;
|
|
1337
|
-
deadFilePaths
|
|
1488
|
+
deadFilePaths = [...result.safeToDelete, ...result.transitivelyDead];
|
|
1338
1489
|
spinner.succeed(
|
|
1339
1490
|
chalk7.green(
|
|
1340
1491
|
`Dead code analysis complete \u2014 ${result.safeToDelete.length} safe to delete, ${result.transitivelyDead.length} transitively dead, ${result.deadExports.length} dead export(s)`
|
|
@@ -1342,23 +1493,26 @@ async function main(opts) {
|
|
|
1342
1493
|
);
|
|
1343
1494
|
if (result.report) {
|
|
1344
1495
|
deadReportFile = "dead-code.txt";
|
|
1345
|
-
|
|
1496
|
+
const banner = `prunify ${PKG_VERSION} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1497
|
+
|
|
1498
|
+
`;
|
|
1499
|
+
writeReport(reportsDir, deadReportFile, banner + result.report);
|
|
1346
1500
|
}
|
|
1347
1501
|
}
|
|
1348
1502
|
if (modules.includes("dupes")) {
|
|
1349
|
-
const outputPath =
|
|
1503
|
+
const outputPath = path10.join(reportsDir, "dupes.md");
|
|
1350
1504
|
const dupes = await runDupeFinder(rootDir, { output: outputPath });
|
|
1351
1505
|
dupeCount = dupes.length;
|
|
1352
1506
|
if (dupeCount > 0) dupesReportFile = "dupes.md";
|
|
1353
1507
|
}
|
|
1354
|
-
if (
|
|
1508
|
+
if (runCircular2) {
|
|
1355
1509
|
const spinner = createSpinner(chalk7.cyan("Analysing circular imports\u2026"));
|
|
1356
1510
|
const cycles = detectCycles(graph);
|
|
1357
1511
|
circularCount = cycles.length;
|
|
1358
1512
|
spinner.succeed(chalk7.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
|
|
1359
1513
|
if (circularCount > 0) {
|
|
1360
1514
|
circularReportFile = "circular.txt";
|
|
1361
|
-
const rel = (p) =>
|
|
1515
|
+
const rel = (p) => path10.relative(rootDir, p).replaceAll("\\", "/");
|
|
1362
1516
|
const cycleLines = [
|
|
1363
1517
|
"========================================",
|
|
1364
1518
|
" CIRCULAR DEPENDENCIES",
|
|
@@ -1376,27 +1530,31 @@ async function main(opts) {
|
|
|
1376
1530
|
cycleLines.push(` \u21BB ${rel(cycle[0])} (back to start)`);
|
|
1377
1531
|
cycleLines.push("");
|
|
1378
1532
|
}
|
|
1379
|
-
|
|
1533
|
+
const banner = `prunify ${PKG_VERSION} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1534
|
+
|
|
1535
|
+
`;
|
|
1536
|
+
writeReport(reportsDir, circularReportFile, banner + cycleLines.join("\n"));
|
|
1380
1537
|
}
|
|
1381
1538
|
}
|
|
1382
1539
|
if (modules.includes("deps")) {
|
|
1383
|
-
const outputPath =
|
|
1540
|
+
const outputPath = path10.join(reportsDir, "deps.md");
|
|
1384
1541
|
const issues = await runDepCheck({ cwd: rootDir, output: outputPath });
|
|
1385
1542
|
unusedPkgCount = issues.filter((i) => i.type === "unused").length;
|
|
1386
1543
|
if (issues.length > 0) depsReportFile = "deps.md";
|
|
1387
1544
|
}
|
|
1388
|
-
if (
|
|
1389
|
-
const outputPath =
|
|
1545
|
+
if (runAssets) {
|
|
1546
|
+
const outputPath = path10.join(reportsDir, "assets.md");
|
|
1390
1547
|
const unusedAssets = await runAssetCheck(rootDir, { output: outputPath });
|
|
1548
|
+
unusedAssetPaths = unusedAssets.map((a) => a.filePath);
|
|
1391
1549
|
unusedAssetCount = unusedAssets.length;
|
|
1392
1550
|
if (unusedAssetCount > 0) assetsReportFile = "assets.md";
|
|
1393
1551
|
}
|
|
1394
1552
|
if (modules.includes("health")) {
|
|
1395
|
-
const outputPath =
|
|
1553
|
+
const outputPath = path10.join(reportsDir, "health-report.md");
|
|
1396
1554
|
await runHealthReport(rootDir, { output: outputPath });
|
|
1397
1555
|
}
|
|
1398
1556
|
if (opts.html) {
|
|
1399
|
-
const htmlPath =
|
|
1557
|
+
const htmlPath = path10.join(reportsDir, "code_health.html");
|
|
1400
1558
|
writeHtmlReport(htmlPath, rootDir, deadFilePaths, circularCount, dupeCount, unusedPkgCount);
|
|
1401
1559
|
console.log(chalk7.cyan(` HTML report written to ${htmlPath}`));
|
|
1402
1560
|
}
|
|
@@ -1418,22 +1576,72 @@ async function main(opts) {
|
|
|
1418
1576
|
console.log(table.toString());
|
|
1419
1577
|
console.log();
|
|
1420
1578
|
if (opts.delete && deadFilePaths.length > 0) {
|
|
1421
|
-
console.log(chalk7.yellow(`Dead files (${deadFilePaths.length}):`));
|
|
1579
|
+
console.log(chalk7.yellow(`Dead code files to delete (${deadFilePaths.length}):`));
|
|
1422
1580
|
for (const f of deadFilePaths) {
|
|
1423
|
-
console.log(chalk7.dim(` ${
|
|
1581
|
+
console.log(chalk7.dim(` ${path10.relative(rootDir, f)}`));
|
|
1424
1582
|
}
|
|
1425
1583
|
console.log();
|
|
1426
1584
|
if (!opts.ci) {
|
|
1427
|
-
const confirmed = await confirmPrompt("Delete these files? (y/N) ");
|
|
1585
|
+
const confirmed = await confirmPrompt("Delete these dead code files? (y/N) ");
|
|
1428
1586
|
if (confirmed) {
|
|
1587
|
+
let removed = 0;
|
|
1429
1588
|
for (const f of deadFilePaths) {
|
|
1430
|
-
|
|
1589
|
+
try {
|
|
1590
|
+
if (fs10.existsSync(f)) {
|
|
1591
|
+
fs10.rmSync(f, { force: true });
|
|
1592
|
+
removed++;
|
|
1593
|
+
}
|
|
1594
|
+
} catch {
|
|
1595
|
+
console.log(chalk7.red(` Failed to delete: ${path10.relative(rootDir, f)}`));
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
console.log(chalk7.green(` Deleted ${removed} file(s).`));
|
|
1599
|
+
} else {
|
|
1600
|
+
console.log(chalk7.dim(" Skipped."));
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
} else if (opts.delete && deadFilePaths.length === 0) {
|
|
1604
|
+
console.log(chalk7.dim(" --delete: no dead code files listed in report (nothing to delete)."));
|
|
1605
|
+
}
|
|
1606
|
+
if (opts.deleteAssets && unusedAssetPaths.length > 0) {
|
|
1607
|
+
const publicDir = path10.join(rootDir, "public");
|
|
1608
|
+
const safePaths = unusedAssetPaths.filter((p) => {
|
|
1609
|
+
const norm = path10.normalize(p);
|
|
1610
|
+
return norm.startsWith(path10.normalize(publicDir + path10.sep));
|
|
1611
|
+
});
|
|
1612
|
+
if (safePaths.length !== unusedAssetPaths.length) {
|
|
1613
|
+
console.log(
|
|
1614
|
+
chalk7.yellow(
|
|
1615
|
+
` Warning: skipped ${unusedAssetPaths.length - safePaths.length} path(s) outside public/`
|
|
1616
|
+
)
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
console.log(chalk7.yellow(`Unused assets to delete (${safePaths.length}):`));
|
|
1620
|
+
for (const f of safePaths) {
|
|
1621
|
+
console.log(chalk7.dim(` ${path10.relative(rootDir, f)}`));
|
|
1622
|
+
}
|
|
1623
|
+
console.log();
|
|
1624
|
+
if (!opts.ci) {
|
|
1625
|
+
const confirmed = await confirmPrompt("Delete these unused assets? (y/N) ");
|
|
1626
|
+
if (confirmed) {
|
|
1627
|
+
let removed = 0;
|
|
1628
|
+
for (const f of safePaths) {
|
|
1629
|
+
try {
|
|
1630
|
+
if (fs10.existsSync(f)) {
|
|
1631
|
+
fs10.rmSync(f, { force: true });
|
|
1632
|
+
removed++;
|
|
1633
|
+
}
|
|
1634
|
+
} catch {
|
|
1635
|
+
console.log(chalk7.red(` Failed to delete: ${path10.relative(rootDir, f)}`));
|
|
1636
|
+
}
|
|
1431
1637
|
}
|
|
1432
|
-
console.log(chalk7.green(` Deleted ${
|
|
1638
|
+
console.log(chalk7.green(` Deleted ${removed} asset file(s).`));
|
|
1433
1639
|
} else {
|
|
1434
1640
|
console.log(chalk7.dim(" Skipped."));
|
|
1435
1641
|
}
|
|
1436
1642
|
}
|
|
1643
|
+
} else if (opts.deleteAssets && unusedAssetPaths.length === 0) {
|
|
1644
|
+
console.log(chalk7.dim(" --delete-assets: no unused assets in report (nothing to delete)."));
|
|
1437
1645
|
}
|
|
1438
1646
|
if (opts.ci) {
|
|
1439
1647
|
const hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0;
|
|
@@ -1447,7 +1655,7 @@ function resolveModules(only) {
|
|
|
1447
1655
|
}
|
|
1448
1656
|
function loadPackageJson2(dir) {
|
|
1449
1657
|
try {
|
|
1450
|
-
return JSON.parse(
|
|
1658
|
+
return JSON.parse(fs10.readFileSync(path10.join(dir, "package.json"), "utf-8"));
|
|
1451
1659
|
} catch {
|
|
1452
1660
|
return null;
|
|
1453
1661
|
}
|
|
@@ -1468,7 +1676,7 @@ function writeHtmlReport(outputPath, rootDir, deadFiles, circularCount, dupeCoun
|
|
|
1468
1676
|
["Circular Dependencies", String(circularCount)],
|
|
1469
1677
|
["Unused Packages", String(unusedPkgCount)]
|
|
1470
1678
|
].map(([label, val]) => ` <tr><td>${label}</td><td>${val}</td></tr>`).join("\n");
|
|
1471
|
-
const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${
|
|
1679
|
+
const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${path10.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
|
|
1472
1680
|
const html = `<!DOCTYPE html>
|
|
1473
1681
|
<html lang="en">
|
|
1474
1682
|
<head>
|
|
@@ -1495,7 +1703,7 @@ ${rows}
|
|
|
1495
1703
|
${deadList}
|
|
1496
1704
|
</body>
|
|
1497
1705
|
</html>`;
|
|
1498
|
-
|
|
1499
|
-
|
|
1706
|
+
fs10.mkdirSync(path10.dirname(outputPath), { recursive: true });
|
|
1707
|
+
fs10.writeFileSync(outputPath, html, "utf-8");
|
|
1500
1708
|
}
|
|
1501
1709
|
//# sourceMappingURL=cli.js.map
|