prunify 0.1.5 → 0.1.7
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 +54 -25
- package/dist/cli.cjs +387 -65
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +387 -65
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -27,8 +27,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
var import_chalk7 = __toESM(require("chalk"), 1);
|
|
29
29
|
var import_cli_table36 = __toESM(require("cli-table3"), 1);
|
|
30
|
-
var
|
|
31
|
-
var
|
|
30
|
+
var import_node_fs10 = __toESM(require("fs"), 1);
|
|
31
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
32
32
|
var import_node_url = require("url");
|
|
33
33
|
var import_node_readline = __toESM(require("readline"), 1);
|
|
34
34
|
|
|
@@ -238,16 +238,93 @@ function findEntryPoints(rootDir, packageJson) {
|
|
|
238
238
|
];
|
|
239
239
|
return [...new Set(entries)];
|
|
240
240
|
}
|
|
241
|
+
var APP_SPECIAL_ENTRY_BASENAMES_LOWER = /* @__PURE__ */ new Set([
|
|
242
|
+
"page.tsx",
|
|
243
|
+
"page.ts",
|
|
244
|
+
"page.jsx",
|
|
245
|
+
"page.js",
|
|
246
|
+
"layout.tsx",
|
|
247
|
+
"layout.ts",
|
|
248
|
+
"layout.jsx",
|
|
249
|
+
"layout.js",
|
|
250
|
+
"template.tsx",
|
|
251
|
+
"template.ts",
|
|
252
|
+
"template.jsx",
|
|
253
|
+
"template.js",
|
|
254
|
+
"loading.tsx",
|
|
255
|
+
"loading.ts",
|
|
256
|
+
"loading.jsx",
|
|
257
|
+
"loading.js",
|
|
258
|
+
"error.tsx",
|
|
259
|
+
"error.ts",
|
|
260
|
+
"error.jsx",
|
|
261
|
+
"error.js",
|
|
262
|
+
"global-error.tsx",
|
|
263
|
+
"global-error.ts",
|
|
264
|
+
"global-error.jsx",
|
|
265
|
+
"global-error.js",
|
|
266
|
+
"not-found.tsx",
|
|
267
|
+
"not-found.ts",
|
|
268
|
+
"not-found.jsx",
|
|
269
|
+
"not-found.js",
|
|
270
|
+
"forbidden.tsx",
|
|
271
|
+
"forbidden.ts",
|
|
272
|
+
"forbidden.jsx",
|
|
273
|
+
"forbidden.js",
|
|
274
|
+
"unauthorized.tsx",
|
|
275
|
+
"unauthorized.ts",
|
|
276
|
+
"unauthorized.jsx",
|
|
277
|
+
"unauthorized.js",
|
|
278
|
+
"default.tsx",
|
|
279
|
+
"default.ts",
|
|
280
|
+
"default.jsx",
|
|
281
|
+
"default.js",
|
|
282
|
+
"route.ts",
|
|
283
|
+
"route.js",
|
|
284
|
+
"opengraph-image.tsx",
|
|
285
|
+
"opengraph-image.ts",
|
|
286
|
+
"opengraph-image.js",
|
|
287
|
+
"twitter-image.tsx",
|
|
288
|
+
"twitter-image.ts",
|
|
289
|
+
"twitter-image.js",
|
|
290
|
+
"icon.tsx",
|
|
291
|
+
"icon.ts",
|
|
292
|
+
"icon.jsx",
|
|
293
|
+
"icon.js",
|
|
294
|
+
"apple-icon.tsx",
|
|
295
|
+
"apple-icon.ts",
|
|
296
|
+
"apple-icon.js",
|
|
297
|
+
"sitemap.ts",
|
|
298
|
+
"sitemap.js",
|
|
299
|
+
"robots.ts",
|
|
300
|
+
"robots.js",
|
|
301
|
+
"manifest.ts",
|
|
302
|
+
"manifest.js"
|
|
303
|
+
]);
|
|
304
|
+
function isConventionRoutedSourceFile(filePath, rootDir) {
|
|
305
|
+
const rel = import_node_path3.default.relative(rootDir, import_node_path3.default.normalize(filePath)).replaceAll("\\", "/");
|
|
306
|
+
if (rel.startsWith("..")) return false;
|
|
307
|
+
if (rel === "pages" || rel.startsWith("pages/")) return true;
|
|
308
|
+
if (rel.startsWith("src/pages/")) return true;
|
|
309
|
+
if (rel.startsWith("src/routes/") || rel.startsWith("src/views/") || rel.startsWith("src/screens/")) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (rel.startsWith("app/") || rel.startsWith("src/app/")) {
|
|
313
|
+
const base = import_node_path3.default.basename(rel).toLowerCase();
|
|
314
|
+
if (APP_SPECIAL_ENTRY_BASENAMES_LOWER.has(base)) return true;
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
241
318
|
function detectCycles(graph) {
|
|
242
319
|
const cycles = [];
|
|
243
320
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
244
321
|
const visited = /* @__PURE__ */ new Set();
|
|
245
322
|
const inStack = /* @__PURE__ */ new Set();
|
|
246
|
-
const
|
|
323
|
+
const path11 = [];
|
|
247
324
|
const acc = { seenKeys, cycles };
|
|
248
325
|
for (const start of graph.keys()) {
|
|
249
326
|
if (!visited.has(start)) {
|
|
250
|
-
dfsForCycles(start, graph, visited, inStack,
|
|
327
|
+
dfsForCycles(start, graph, visited, inStack, path11, acc);
|
|
251
328
|
}
|
|
252
329
|
}
|
|
253
330
|
return cycles;
|
|
@@ -305,7 +382,7 @@ function resolveFallbackEntries(rootDir) {
|
|
|
305
382
|
function mkFrame(node, graph) {
|
|
306
383
|
return { node, neighbors: (graph.get(node) ?? /* @__PURE__ */ new Set()).values(), entered: false };
|
|
307
384
|
}
|
|
308
|
-
function dfsForCycles(start, graph, visited, inStack,
|
|
385
|
+
function dfsForCycles(start, graph, visited, inStack, path11, acc) {
|
|
309
386
|
const stack = [mkFrame(start, graph)];
|
|
310
387
|
while (stack.length > 0) {
|
|
311
388
|
const frame = stack.at(-1);
|
|
@@ -317,30 +394,30 @@ function dfsForCycles(start, graph, visited, inStack, path10, acc) {
|
|
|
317
394
|
}
|
|
318
395
|
frame.entered = true;
|
|
319
396
|
inStack.add(frame.node);
|
|
320
|
-
|
|
397
|
+
path11.push(frame.node);
|
|
321
398
|
}
|
|
322
399
|
const { done, value: neighbor } = frame.neighbors.next();
|
|
323
400
|
if (done) {
|
|
324
401
|
stack.pop();
|
|
325
|
-
|
|
402
|
+
path11.pop();
|
|
326
403
|
inStack.delete(frame.node);
|
|
327
404
|
visited.add(frame.node);
|
|
328
405
|
} else {
|
|
329
|
-
handleCycleNeighbor(neighbor, stack,
|
|
406
|
+
handleCycleNeighbor(neighbor, stack, path11, inStack, visited, acc, graph);
|
|
330
407
|
}
|
|
331
408
|
}
|
|
332
409
|
}
|
|
333
|
-
function handleCycleNeighbor(neighbor, stack,
|
|
410
|
+
function handleCycleNeighbor(neighbor, stack, path11, inStack, visited, acc, graph) {
|
|
334
411
|
if (inStack.has(neighbor)) {
|
|
335
|
-
recordCycle(neighbor,
|
|
412
|
+
recordCycle(neighbor, path11, acc);
|
|
336
413
|
} else if (!visited.has(neighbor)) {
|
|
337
414
|
stack.push(mkFrame(neighbor, graph));
|
|
338
415
|
}
|
|
339
416
|
}
|
|
340
|
-
function recordCycle(cycleStart,
|
|
341
|
-
const idx =
|
|
417
|
+
function recordCycle(cycleStart, path11, acc) {
|
|
418
|
+
const idx = path11.indexOf(cycleStart);
|
|
342
419
|
if (idx === -1) return;
|
|
343
|
-
const cycle = normalizeCycle(
|
|
420
|
+
const cycle = normalizeCycle(path11.slice(idx));
|
|
344
421
|
const key = cycle.join("\0");
|
|
345
422
|
if (!acc.seenKeys.has(key)) {
|
|
346
423
|
acc.seenKeys.add(key);
|
|
@@ -534,7 +611,7 @@ function runDeadCodeModule(project, graph, entryPoints, rootDir) {
|
|
|
534
611
|
chains.set(root, chain);
|
|
535
612
|
}
|
|
536
613
|
const liveFiles = new Set(allFiles.filter((f) => !deadSet.has(f)));
|
|
537
|
-
const deadExports = findDeadExports(project, liveFiles);
|
|
614
|
+
const deadExports = findDeadExports(project, liveFiles, rootDir);
|
|
538
615
|
const report = buildDeadCodeReport(
|
|
539
616
|
safeToDelete,
|
|
540
617
|
transitivelyDead,
|
|
@@ -569,10 +646,12 @@ function collectTransitiveChain(root, graph, deadSet) {
|
|
|
569
646
|
}
|
|
570
647
|
return chain;
|
|
571
648
|
}
|
|
572
|
-
function findDeadExports(project, liveFiles) {
|
|
649
|
+
function findDeadExports(project, liveFiles, rootDir) {
|
|
573
650
|
const importedNames = buildImportedNameMap(project, liveFiles);
|
|
574
651
|
const dead = [];
|
|
575
652
|
for (const filePath of liveFiles) {
|
|
653
|
+
if (isFrameworkFile(filePath, rootDir)) continue;
|
|
654
|
+
if (isConventionRoutedSourceFile(filePath, rootDir)) continue;
|
|
576
655
|
collectFileDeadExports(filePath, project, importedNames, dead);
|
|
577
656
|
}
|
|
578
657
|
return dead;
|
|
@@ -646,7 +725,7 @@ function buildDeadCodeReport(safeToDelete, transitivelyDead, chains, deadExports
|
|
|
646
725
|
if (deadExports.length > 0) {
|
|
647
726
|
lines.push(
|
|
648
727
|
"\u2500\u2500 DEAD EXPORTS \u2500\u2500",
|
|
649
|
-
"(Exported but never imported by any other file)",
|
|
728
|
+
"(Exported but never imported by any other file; pages/routes omitted)",
|
|
650
729
|
""
|
|
651
730
|
);
|
|
652
731
|
for (const entry of deadExports) {
|
|
@@ -1284,21 +1363,138 @@ function buildAssetReport(unused, totalAssets, rootDir) {
|
|
|
1284
1363
|
return lines.join("\n");
|
|
1285
1364
|
}
|
|
1286
1365
|
|
|
1366
|
+
// src/core/report-parser.ts
|
|
1367
|
+
var import_node_fs9 = __toESM(require("fs"), 1);
|
|
1368
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1369
|
+
function parseDeadCodeReportFile(reportPath, rootDir) {
|
|
1370
|
+
if (!import_node_fs9.default.existsSync(reportPath)) return [];
|
|
1371
|
+
const content = import_node_fs9.default.readFileSync(reportPath, "utf-8");
|
|
1372
|
+
return parseDeadCodeReportContent(content, rootDir);
|
|
1373
|
+
}
|
|
1374
|
+
function parseDeadCodeReportContent(content, rootDir) {
|
|
1375
|
+
const lines = content.split(/\r?\n/);
|
|
1376
|
+
const paths = [];
|
|
1377
|
+
const legacyRe = /^DEAD FILE —\s+(.+)$/;
|
|
1378
|
+
let legacyMode = content.includes("DEAD FILE \u2014") && content.includes("Dead files :");
|
|
1379
|
+
if (legacyMode) {
|
|
1380
|
+
for (const line of lines) {
|
|
1381
|
+
const m = legacyRe.exec(line.trim());
|
|
1382
|
+
if (m) paths.push(import_node_path9.default.resolve(rootDir, m[1].trim()));
|
|
1383
|
+
}
|
|
1384
|
+
return [...new Set(paths)];
|
|
1385
|
+
}
|
|
1386
|
+
let section = "none";
|
|
1387
|
+
const fileLineRe = /^\s{2}(.+?)\s+\(~[\d.]+\s+KB\)\s*$/;
|
|
1388
|
+
for (const line of lines) {
|
|
1389
|
+
if (line.includes("\u2500\u2500 SAFE TO DELETE \u2500\u2500")) {
|
|
1390
|
+
section = "safe";
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
if (line.includes("\u2500\u2500 TRANSITIVELY DEAD \u2500\u2500")) {
|
|
1394
|
+
section = "transitive";
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
if (line.includes("\u2500\u2500 DEAD EXPORTS \u2500\u2500")) {
|
|
1398
|
+
section = "none";
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
if (line.startsWith("\u2500\u2500 ") && line.includes("\u2500\u2500")) {
|
|
1402
|
+
if (section !== "none" && !line.includes("SAFE TO DELETE") && !line.includes("TRANSITIVELY DEAD")) {
|
|
1403
|
+
section = "none";
|
|
1404
|
+
}
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
if (section === "safe" || section === "transitive") {
|
|
1408
|
+
if (line.includes("\u2514\u2500") || line.includes("also makes dead")) continue;
|
|
1409
|
+
const m = fileLineRe.exec(line);
|
|
1410
|
+
if (m) {
|
|
1411
|
+
const rel = m[1].trim();
|
|
1412
|
+
if (rel && !rel.startsWith("(")) {
|
|
1413
|
+
paths.push(import_node_path9.default.resolve(rootDir, rel));
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return [...new Set(paths)];
|
|
1419
|
+
}
|
|
1420
|
+
function countDeadExportsInReport(content) {
|
|
1421
|
+
let section = false;
|
|
1422
|
+
let n = 0;
|
|
1423
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1424
|
+
if (line.includes("\u2500\u2500 DEAD EXPORTS \u2500\u2500")) {
|
|
1425
|
+
section = true;
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
if (section && line.startsWith("\u2500\u2500 ") && line.includes("\u2500\u2500")) break;
|
|
1429
|
+
if (section && /\s→\s.+\[line \d+\]/.test(line)) n++;
|
|
1430
|
+
}
|
|
1431
|
+
return n;
|
|
1432
|
+
}
|
|
1433
|
+
function parseDeadCodeReportSummary(content) {
|
|
1434
|
+
const safe = content.match(/Safe to delete\s*:\s*(\d+)/);
|
|
1435
|
+
const trans = content.match(/Transitively dead\s*:\s*(\d+)/);
|
|
1436
|
+
const exp = content.match(/Dead exports\s*:\s*(\d+)/);
|
|
1437
|
+
const rec = content.match(/Recoverable\s*:\s*(~[\d.]+\s*KB)/i);
|
|
1438
|
+
if (!safe || !trans || !exp) return null;
|
|
1439
|
+
return {
|
|
1440
|
+
safeToDelete: Number(safe[1]),
|
|
1441
|
+
transitivelyDead: Number(trans[1]),
|
|
1442
|
+
deadExports: Number(exp[1]),
|
|
1443
|
+
recoverableDisplay: rec ? rec[1].trim() : "~0.0 KB"
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function parseAssetsReportFile(reportPath, rootDir) {
|
|
1447
|
+
if (!import_node_fs9.default.existsSync(reportPath)) return [];
|
|
1448
|
+
const content = import_node_fs9.default.readFileSync(reportPath, "utf-8");
|
|
1449
|
+
return parseAssetsReportContent(content, rootDir);
|
|
1450
|
+
}
|
|
1451
|
+
function parseAssetsReportContent(content, rootDir) {
|
|
1452
|
+
const lines = content.split(/\r?\n/);
|
|
1453
|
+
const paths = [];
|
|
1454
|
+
let inTable = false;
|
|
1455
|
+
let headerSeen = false;
|
|
1456
|
+
for (const line of lines) {
|
|
1457
|
+
const trimmed = line.trim();
|
|
1458
|
+
if (trimmed.startsWith("## Unused Assets")) {
|
|
1459
|
+
inTable = true;
|
|
1460
|
+
headerSeen = false;
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (inTable && trimmed.startsWith("## ")) {
|
|
1464
|
+
break;
|
|
1465
|
+
}
|
|
1466
|
+
if (!inTable) continue;
|
|
1467
|
+
if (trimmed.startsWith("|") && trimmed.includes("---")) {
|
|
1468
|
+
headerSeen = true;
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
if (!headerSeen || !trimmed.startsWith("|")) continue;
|
|
1472
|
+
const cells = trimmed.split("|").map((c) => c.trim()).filter(Boolean);
|
|
1473
|
+
if (cells.length >= 1) {
|
|
1474
|
+
const rel = cells[0];
|
|
1475
|
+
if (rel.toLowerCase() === "asset") continue;
|
|
1476
|
+
if (rel.includes("---")) continue;
|
|
1477
|
+
paths.push(import_node_path9.default.resolve(rootDir, rel));
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return [...new Set(paths)];
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1287
1483
|
// src/cli.ts
|
|
1288
1484
|
var import_meta = {};
|
|
1289
1485
|
function readPkgVersion() {
|
|
1290
1486
|
try {
|
|
1291
1487
|
if (typeof import_meta !== "undefined" && import_meta.url) {
|
|
1292
|
-
const dir =
|
|
1293
|
-
const pkgPath =
|
|
1294
|
-
return JSON.parse(
|
|
1488
|
+
const dir = import_node_path10.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
1489
|
+
const pkgPath = import_node_path10.default.resolve(dir, "..", "package.json");
|
|
1490
|
+
return JSON.parse(import_node_fs10.default.readFileSync(pkgPath, "utf-8")).version;
|
|
1295
1491
|
}
|
|
1296
1492
|
} catch {
|
|
1297
1493
|
}
|
|
1298
1494
|
try {
|
|
1299
1495
|
const dir = globalThis.__dirname ?? __dirname;
|
|
1300
|
-
const pkgPath =
|
|
1301
|
-
return JSON.parse(
|
|
1496
|
+
const pkgPath = import_node_path10.default.resolve(dir, "..", "package.json");
|
|
1497
|
+
return JSON.parse(import_node_fs10.default.readFileSync(pkgPath, "utf-8")).version;
|
|
1302
1498
|
} catch {
|
|
1303
1499
|
return "0.0.0";
|
|
1304
1500
|
}
|
|
@@ -1311,11 +1507,14 @@ program.name("prunify").description("npm run clean. ship with confidence.").vers
|
|
|
1311
1507
|
"Glob pattern to ignore (repeatable)",
|
|
1312
1508
|
(val, acc) => [...acc, val],
|
|
1313
1509
|
[]
|
|
1314
|
-
).option("--out <path>", "Output directory for reports").option("--html", "Also generate code_health.html").option("--delete", "
|
|
1510
|
+
).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(
|
|
1511
|
+
"--delete-assets",
|
|
1512
|
+
"Delete unused public assets (uses prunify-reports/assets.md if present, else runs asset scan)"
|
|
1513
|
+
).option("--ci", "CI mode: exit 1 if issues found, no interactive prompts").action(main);
|
|
1315
1514
|
program.parse();
|
|
1316
1515
|
async function main(opts) {
|
|
1317
|
-
const rootDir =
|
|
1318
|
-
if (!
|
|
1516
|
+
const rootDir = import_node_path10.default.resolve(opts.dir);
|
|
1517
|
+
if (!import_node_fs10.default.existsSync(import_node_path10.default.join(rootDir, "package.json"))) {
|
|
1319
1518
|
console.error(import_chalk7.default.red(`\u2717 No package.json found in ${rootDir}`));
|
|
1320
1519
|
console.error(import_chalk7.default.dim(" Use --dir <path> to point to your project root."));
|
|
1321
1520
|
process.exit(1);
|
|
@@ -1324,65 +1523,124 @@ async function main(opts) {
|
|
|
1324
1523
|
console.log();
|
|
1325
1524
|
console.log(import_chalk7.default.bold.cyan("\u{1F9F9} prunify \u2014 npm run clean. ship with confidence."));
|
|
1326
1525
|
console.log();
|
|
1327
|
-
const parseSpinner = createSpinner(import_chalk7.default.cyan("Parsing codebase\u2026"));
|
|
1328
|
-
const files = discoverFiles(rootDir, opts.ignore);
|
|
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"));
|
|
1331
|
-
const project = buildProject(files);
|
|
1332
|
-
const graph = buildGraph(files, (f) => {
|
|
1333
|
-
const sf = project.getSourceFile(f);
|
|
1334
|
-
return sf ? getImportsForFile(sf) : [];
|
|
1335
|
-
});
|
|
1336
|
-
const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0);
|
|
1337
|
-
graphSpinner.succeed(import_chalk7.default.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
|
|
1338
|
-
const packageJson = loadPackageJson2(rootDir);
|
|
1339
|
-
const entryPoints = opts.entry ? [import_node_path9.default.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
|
|
1340
1526
|
const reportsDir = ensureReportsDir(rootDir, opts.out);
|
|
1341
1527
|
appendToGitignore(rootDir);
|
|
1528
|
+
const deadReportPath = import_node_path10.default.join(reportsDir, "dead-code.txt");
|
|
1529
|
+
const assetsReportPath = import_node_path10.default.join(reportsDir, "assets.md");
|
|
1530
|
+
const loadDeadFromCache = Boolean(opts.delete && import_node_fs10.default.existsSync(deadReportPath));
|
|
1531
|
+
const loadAssetsFromCache = Boolean(opts.deleteAssets && import_node_fs10.default.existsSync(assetsReportPath));
|
|
1532
|
+
let deadFilePaths = [];
|
|
1533
|
+
let unusedAssetPaths = [];
|
|
1534
|
+
if (loadDeadFromCache) {
|
|
1535
|
+
deadFilePaths = parseDeadCodeReportFile(deadReportPath, rootDir);
|
|
1536
|
+
console.log(
|
|
1537
|
+
import_chalk7.default.cyan(
|
|
1538
|
+
` Using existing dead-code report (${deadFilePaths.length} file(s) to delete) \u2192 ${import_node_path10.default.relative(rootDir, deadReportPath)}`
|
|
1539
|
+
)
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
if (loadAssetsFromCache) {
|
|
1543
|
+
unusedAssetPaths = parseAssetsReportFile(assetsReportPath, rootDir);
|
|
1544
|
+
console.log(
|
|
1545
|
+
import_chalk7.default.cyan(
|
|
1546
|
+
` Using existing assets report (${unusedAssetPaths.length} asset(s) to delete) \u2192 ${import_node_path10.default.relative(rootDir, assetsReportPath)}`
|
|
1547
|
+
)
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
const runDeadCode2 = !loadDeadFromCache && (modules.includes("dead-code") || Boolean(opts.delete));
|
|
1551
|
+
const runCircular2 = modules.includes("circular");
|
|
1552
|
+
const runAssets = !loadAssetsFromCache && (modules.includes("assets") || Boolean(opts.deleteAssets));
|
|
1553
|
+
const needsGraph = runCircular2 || runDeadCode2;
|
|
1554
|
+
let files = [];
|
|
1555
|
+
let project;
|
|
1556
|
+
let graph;
|
|
1557
|
+
let entryPoints = [];
|
|
1558
|
+
if (needsGraph) {
|
|
1559
|
+
const parseSpinner = createSpinner(import_chalk7.default.cyan("Parsing codebase\u2026"));
|
|
1560
|
+
files = discoverFiles(rootDir, opts.ignore);
|
|
1561
|
+
parseSpinner.succeed(import_chalk7.default.green(`Parsed codebase \u2014 ${files.length} file(s) found`));
|
|
1562
|
+
const graphSpinner = createSpinner(import_chalk7.default.cyan("Building import graph\u2026"));
|
|
1563
|
+
project = buildProject(files);
|
|
1564
|
+
graph = buildGraph(files, (f) => {
|
|
1565
|
+
const sf = project.getSourceFile(f);
|
|
1566
|
+
return sf ? getImportsForFile(sf) : [];
|
|
1567
|
+
});
|
|
1568
|
+
const edgeCount = [...graph.values()].reduce((n, s) => n + s.size, 0);
|
|
1569
|
+
graphSpinner.succeed(import_chalk7.default.green(`Import graph built \u2014 ${edgeCount} edge(s)`));
|
|
1570
|
+
const packageJson = loadPackageJson2(rootDir);
|
|
1571
|
+
entryPoints = opts.entry ? [import_node_path10.default.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
|
|
1572
|
+
}
|
|
1342
1573
|
console.log();
|
|
1343
|
-
let
|
|
1574
|
+
let deadSafeCount = 0;
|
|
1575
|
+
let deadTransitiveCount = 0;
|
|
1576
|
+
let deadExportsCount = 0;
|
|
1577
|
+
let deadRecoverDisplay = "~0.0 KB";
|
|
1344
1578
|
let dupeCount = 0;
|
|
1345
1579
|
let unusedPkgCount = 0;
|
|
1346
1580
|
let circularCount = 0;
|
|
1347
1581
|
let unusedAssetCount = 0;
|
|
1348
|
-
let deadReportFile = "";
|
|
1349
1582
|
let dupesReportFile = "";
|
|
1350
1583
|
let depsReportFile = "";
|
|
1351
1584
|
let circularReportFile = "";
|
|
1352
1585
|
let assetsReportFile = "";
|
|
1353
|
-
|
|
1354
|
-
|
|
1586
|
+
if (loadDeadFromCache) {
|
|
1587
|
+
try {
|
|
1588
|
+
const raw = import_node_fs10.default.readFileSync(deadReportPath, "utf-8");
|
|
1589
|
+
const parsed = parseDeadCodeReportSummary(raw);
|
|
1590
|
+
if (parsed) {
|
|
1591
|
+
deadSafeCount = parsed.safeToDelete;
|
|
1592
|
+
deadTransitiveCount = parsed.transitivelyDead;
|
|
1593
|
+
deadExportsCount = parsed.deadExports;
|
|
1594
|
+
deadRecoverDisplay = parsed.recoverableDisplay;
|
|
1595
|
+
} else {
|
|
1596
|
+
deadExportsCount = countDeadExportsInReport(raw);
|
|
1597
|
+
deadSafeCount = deadFilePaths.length;
|
|
1598
|
+
deadTransitiveCount = 0;
|
|
1599
|
+
deadRecoverDisplay = "\u2014";
|
|
1600
|
+
}
|
|
1601
|
+
} catch {
|
|
1602
|
+
deadSafeCount = deadFilePaths.length;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (loadAssetsFromCache) {
|
|
1606
|
+
assetsReportFile = "assets.md";
|
|
1607
|
+
unusedAssetCount = unusedAssetPaths.length;
|
|
1608
|
+
}
|
|
1609
|
+
if (runDeadCode2) {
|
|
1355
1610
|
const spinner = createSpinner(import_chalk7.default.cyan("Analysing dead code\u2026"));
|
|
1356
1611
|
const result = runDeadCodeModule(project, graph, entryPoints, rootDir);
|
|
1357
|
-
|
|
1358
|
-
|
|
1612
|
+
deadFilePaths = [...result.safeToDelete, ...result.transitivelyDead];
|
|
1613
|
+
deadSafeCount = result.safeToDelete.length;
|
|
1614
|
+
deadTransitiveCount = result.transitivelyDead.length;
|
|
1615
|
+
deadExportsCount = result.deadExports.length;
|
|
1616
|
+
const recoverBytes = deadFilePaths.reduce((sum, f) => sum + getFileSize(f), 0);
|
|
1617
|
+
deadRecoverDisplay = `~${(recoverBytes / 1024).toFixed(1)} KB`;
|
|
1359
1618
|
spinner.succeed(
|
|
1360
1619
|
import_chalk7.default.green(
|
|
1361
1620
|
`Dead code analysis complete \u2014 ${result.safeToDelete.length} safe to delete, ${result.transitivelyDead.length} transitively dead, ${result.deadExports.length} dead export(s)`
|
|
1362
1621
|
)
|
|
1363
1622
|
);
|
|
1364
1623
|
if (result.report) {
|
|
1365
|
-
deadReportFile = "dead-code.txt";
|
|
1366
1624
|
const banner = `prunify ${PKG_VERSION} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1367
1625
|
|
|
1368
1626
|
`;
|
|
1369
|
-
writeReport(reportsDir,
|
|
1627
|
+
writeReport(reportsDir, "dead-code.txt", banner + result.report);
|
|
1370
1628
|
}
|
|
1371
1629
|
}
|
|
1372
1630
|
if (modules.includes("dupes")) {
|
|
1373
|
-
const outputPath =
|
|
1631
|
+
const outputPath = import_node_path10.default.join(reportsDir, "dupes.md");
|
|
1374
1632
|
const dupes = await runDupeFinder(rootDir, { output: outputPath });
|
|
1375
1633
|
dupeCount = dupes.length;
|
|
1376
1634
|
if (dupeCount > 0) dupesReportFile = "dupes.md";
|
|
1377
1635
|
}
|
|
1378
|
-
if (
|
|
1636
|
+
if (runCircular2) {
|
|
1379
1637
|
const spinner = createSpinner(import_chalk7.default.cyan("Analysing circular imports\u2026"));
|
|
1380
1638
|
const cycles = detectCycles(graph);
|
|
1381
1639
|
circularCount = cycles.length;
|
|
1382
1640
|
spinner.succeed(import_chalk7.default.green(`Circular import analysis complete \u2014 ${circularCount} cycle(s) found`));
|
|
1383
1641
|
if (circularCount > 0) {
|
|
1384
1642
|
circularReportFile = "circular.txt";
|
|
1385
|
-
const rel = (p) =>
|
|
1643
|
+
const rel = (p) => import_node_path10.default.relative(rootDir, p).replaceAll("\\", "/");
|
|
1386
1644
|
const cycleLines = [
|
|
1387
1645
|
"========================================",
|
|
1388
1646
|
" CIRCULAR DEPENDENCIES",
|
|
@@ -1407,36 +1665,49 @@ async function main(opts) {
|
|
|
1407
1665
|
}
|
|
1408
1666
|
}
|
|
1409
1667
|
if (modules.includes("deps")) {
|
|
1410
|
-
const outputPath =
|
|
1668
|
+
const outputPath = import_node_path10.default.join(reportsDir, "deps.md");
|
|
1411
1669
|
const issues = await runDepCheck({ cwd: rootDir, output: outputPath });
|
|
1412
1670
|
unusedPkgCount = issues.filter((i) => i.type === "unused").length;
|
|
1413
1671
|
if (issues.length > 0) depsReportFile = "deps.md";
|
|
1414
1672
|
}
|
|
1415
|
-
if (
|
|
1416
|
-
const outputPath =
|
|
1673
|
+
if (runAssets) {
|
|
1674
|
+
const outputPath = import_node_path10.default.join(reportsDir, "assets.md");
|
|
1417
1675
|
const unusedAssets = await runAssetCheck(rootDir, { output: outputPath });
|
|
1676
|
+
unusedAssetPaths = unusedAssets.map((a) => a.filePath);
|
|
1418
1677
|
unusedAssetCount = unusedAssets.length;
|
|
1419
1678
|
if (unusedAssetCount > 0) assetsReportFile = "assets.md";
|
|
1420
1679
|
}
|
|
1421
1680
|
if (modules.includes("health")) {
|
|
1422
|
-
const outputPath =
|
|
1681
|
+
const outputPath = import_node_path10.default.join(reportsDir, "health-report.md");
|
|
1423
1682
|
await runHealthReport(rootDir, { output: outputPath });
|
|
1424
1683
|
}
|
|
1425
1684
|
if (opts.html) {
|
|
1426
|
-
const htmlPath =
|
|
1685
|
+
const htmlPath = import_node_path10.default.join(reportsDir, "code_health.html");
|
|
1427
1686
|
writeHtmlReport(htmlPath, rootDir, deadFilePaths, circularCount, dupeCount, unusedPkgCount);
|
|
1428
1687
|
console.log(import_chalk7.default.cyan(` HTML report written to ${htmlPath}`));
|
|
1429
1688
|
}
|
|
1430
1689
|
console.log();
|
|
1431
1690
|
console.log(import_chalk7.default.bold("Summary"));
|
|
1432
1691
|
console.log();
|
|
1692
|
+
const fmt = (n) => n > 0 ? import_chalk7.default.yellow(String(n)) : import_chalk7.default.green("0");
|
|
1693
|
+
const showDeadSummary = modules.includes("dead-code") || Boolean(opts.delete) || loadDeadFromCache || runDeadCode2;
|
|
1694
|
+
if (showDeadSummary) {
|
|
1695
|
+
const recoverKb = parseFloat(deadRecoverDisplay.replace(/[^\d.]/g, "")) || 0;
|
|
1696
|
+
const recoverCh = deadRecoverDisplay === "\u2014" ? import_chalk7.default.dim("\u2014") : recoverKb > 0 ? import_chalk7.default.yellow(deadRecoverDisplay) : import_chalk7.default.green(deadRecoverDisplay);
|
|
1697
|
+
console.log(import_chalk7.default.dim("========================================"));
|
|
1698
|
+
console.log(import_chalk7.default.dim(" DEAD CODE REPORT"));
|
|
1699
|
+
console.log(import_chalk7.default.dim(" Safe to delete : ") + fmt(deadSafeCount));
|
|
1700
|
+
console.log(import_chalk7.default.dim(" Transitively dead : ") + fmt(deadTransitiveCount));
|
|
1701
|
+
console.log(import_chalk7.default.dim(" Dead exports : ") + fmt(deadExportsCount));
|
|
1702
|
+
console.log(import_chalk7.default.dim(" Recoverable : ") + recoverCh);
|
|
1703
|
+
console.log(import_chalk7.default.dim("========================================"));
|
|
1704
|
+
console.log();
|
|
1705
|
+
}
|
|
1433
1706
|
const table = new import_cli_table36.default({
|
|
1434
1707
|
head: [import_chalk7.default.bold("Check"), import_chalk7.default.bold("Found"), import_chalk7.default.bold("Output File")],
|
|
1435
1708
|
style: { head: [], border: [] }
|
|
1436
1709
|
});
|
|
1437
|
-
const fmt = (n) => n > 0 ? import_chalk7.default.yellow(String(n)) : import_chalk7.default.green("0");
|
|
1438
1710
|
table.push(
|
|
1439
|
-
["Dead Code (files + exports)", fmt(deadFileCount), deadReportFile || "\u2014"],
|
|
1440
1711
|
["Circular Dependencies", fmt(circularCount), circularReportFile || "\u2014"],
|
|
1441
1712
|
["Duplicate Clusters", fmt(dupeCount), dupesReportFile || "\u2014"],
|
|
1442
1713
|
["Unused Packages", fmt(unusedPkgCount), depsReportFile || "\u2014"],
|
|
@@ -1445,25 +1716,76 @@ async function main(opts) {
|
|
|
1445
1716
|
console.log(table.toString());
|
|
1446
1717
|
console.log();
|
|
1447
1718
|
if (opts.delete && deadFilePaths.length > 0) {
|
|
1448
|
-
console.log(import_chalk7.default.yellow(`Dead files (${deadFilePaths.length}):`));
|
|
1719
|
+
console.log(import_chalk7.default.yellow(`Dead code files to delete (${deadFilePaths.length}):`));
|
|
1449
1720
|
for (const f of deadFilePaths) {
|
|
1450
|
-
console.log(import_chalk7.default.dim(` ${
|
|
1721
|
+
console.log(import_chalk7.default.dim(` ${import_node_path10.default.relative(rootDir, f)}`));
|
|
1451
1722
|
}
|
|
1452
1723
|
console.log();
|
|
1453
1724
|
if (!opts.ci) {
|
|
1454
|
-
const confirmed = await confirmPrompt("Delete these files? (y/N) ");
|
|
1725
|
+
const confirmed = await confirmPrompt("Delete these dead code files? (y/N) ");
|
|
1455
1726
|
if (confirmed) {
|
|
1727
|
+
let removed = 0;
|
|
1456
1728
|
for (const f of deadFilePaths) {
|
|
1457
|
-
|
|
1729
|
+
try {
|
|
1730
|
+
if (import_node_fs10.default.existsSync(f)) {
|
|
1731
|
+
import_node_fs10.default.rmSync(f, { force: true });
|
|
1732
|
+
removed++;
|
|
1733
|
+
}
|
|
1734
|
+
} catch {
|
|
1735
|
+
console.log(import_chalk7.default.red(` Failed to delete: ${import_node_path10.default.relative(rootDir, f)}`));
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
console.log(import_chalk7.default.green(` Deleted ${removed} file(s).`));
|
|
1739
|
+
} else {
|
|
1740
|
+
console.log(import_chalk7.default.dim(" Skipped."));
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
} else if (opts.delete && deadFilePaths.length === 0) {
|
|
1744
|
+
console.log(import_chalk7.default.dim(" --delete: no dead code files listed in report (nothing to delete)."));
|
|
1745
|
+
}
|
|
1746
|
+
if (opts.deleteAssets && unusedAssetPaths.length > 0) {
|
|
1747
|
+
const publicDir = import_node_path10.default.join(rootDir, "public");
|
|
1748
|
+
const safePaths = unusedAssetPaths.filter((p) => {
|
|
1749
|
+
const norm = import_node_path10.default.normalize(p);
|
|
1750
|
+
return norm.startsWith(import_node_path10.default.normalize(publicDir + import_node_path10.default.sep));
|
|
1751
|
+
});
|
|
1752
|
+
if (safePaths.length !== unusedAssetPaths.length) {
|
|
1753
|
+
console.log(
|
|
1754
|
+
import_chalk7.default.yellow(
|
|
1755
|
+
` Warning: skipped ${unusedAssetPaths.length - safePaths.length} path(s) outside public/`
|
|
1756
|
+
)
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
console.log(import_chalk7.default.yellow(`Unused assets to delete (${safePaths.length}):`));
|
|
1760
|
+
for (const f of safePaths) {
|
|
1761
|
+
console.log(import_chalk7.default.dim(` ${import_node_path10.default.relative(rootDir, f)}`));
|
|
1762
|
+
}
|
|
1763
|
+
console.log();
|
|
1764
|
+
if (!opts.ci) {
|
|
1765
|
+
const confirmed = await confirmPrompt("Delete these unused assets? (y/N) ");
|
|
1766
|
+
if (confirmed) {
|
|
1767
|
+
let removed = 0;
|
|
1768
|
+
for (const f of safePaths) {
|
|
1769
|
+
try {
|
|
1770
|
+
if (import_node_fs10.default.existsSync(f)) {
|
|
1771
|
+
import_node_fs10.default.rmSync(f, { force: true });
|
|
1772
|
+
removed++;
|
|
1773
|
+
}
|
|
1774
|
+
} catch {
|
|
1775
|
+
console.log(import_chalk7.default.red(` Failed to delete: ${import_node_path10.default.relative(rootDir, f)}`));
|
|
1776
|
+
}
|
|
1458
1777
|
}
|
|
1459
|
-
console.log(import_chalk7.default.green(` Deleted ${
|
|
1778
|
+
console.log(import_chalk7.default.green(` Deleted ${removed} asset file(s).`));
|
|
1460
1779
|
} else {
|
|
1461
1780
|
console.log(import_chalk7.default.dim(" Skipped."));
|
|
1462
1781
|
}
|
|
1463
1782
|
}
|
|
1783
|
+
} else if (opts.deleteAssets && unusedAssetPaths.length === 0) {
|
|
1784
|
+
console.log(import_chalk7.default.dim(" --delete-assets: no unused assets in report (nothing to delete)."));
|
|
1464
1785
|
}
|
|
1465
1786
|
if (opts.ci) {
|
|
1466
|
-
const
|
|
1787
|
+
const deadIssueCount = deadSafeCount + deadTransitiveCount + deadExportsCount;
|
|
1788
|
+
const hasIssues = deadIssueCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0;
|
|
1467
1789
|
if (hasIssues) process.exit(1);
|
|
1468
1790
|
}
|
|
1469
1791
|
}
|
|
@@ -1474,7 +1796,7 @@ function resolveModules(only) {
|
|
|
1474
1796
|
}
|
|
1475
1797
|
function loadPackageJson2(dir) {
|
|
1476
1798
|
try {
|
|
1477
|
-
return JSON.parse(
|
|
1799
|
+
return JSON.parse(import_node_fs10.default.readFileSync(import_node_path10.default.join(dir, "package.json"), "utf-8"));
|
|
1478
1800
|
} catch {
|
|
1479
1801
|
return null;
|
|
1480
1802
|
}
|
|
@@ -1495,7 +1817,7 @@ function writeHtmlReport(outputPath, rootDir, deadFiles, circularCount, dupeCoun
|
|
|
1495
1817
|
["Circular Dependencies", String(circularCount)],
|
|
1496
1818
|
["Unused Packages", String(unusedPkgCount)]
|
|
1497
1819
|
].map(([label, val]) => ` <tr><td>${label}</td><td>${val}</td></tr>`).join("\n");
|
|
1498
|
-
const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${
|
|
1820
|
+
const deadList = deadFiles.length > 0 ? `<ul>${deadFiles.map((f) => `<li>${import_node_path10.default.relative(rootDir, f)}</li>`).join("")}</ul>` : "<p>None</p>";
|
|
1499
1821
|
const html = `<!DOCTYPE html>
|
|
1500
1822
|
<html lang="en">
|
|
1501
1823
|
<head>
|
|
@@ -1522,7 +1844,7 @@ ${rows}
|
|
|
1522
1844
|
${deadList}
|
|
1523
1845
|
</body>
|
|
1524
1846
|
</html>`;
|
|
1525
|
-
|
|
1526
|
-
|
|
1847
|
+
import_node_fs10.default.mkdirSync(import_node_path10.default.dirname(outputPath), { recursive: true });
|
|
1848
|
+
import_node_fs10.default.writeFileSync(outputPath, html, "utf-8");
|
|
1527
1849
|
}
|
|
1528
1850
|
//# sourceMappingURL=cli.cjs.map
|