prunify 0.1.6 → 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 +17 -10
- package/dist/cli.cjs +134 -14
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +134 -14
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,15 +89,22 @@ npx prunify --ci
|
|
|
89
89
|
|
|
90
90
|
Summary
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
========================================
|
|
93
|
+
DEAD CODE REPORT
|
|
94
|
+
Safe to delete : 3
|
|
95
|
+
Transitively dead : 1
|
|
96
|
+
Dead exports : 3
|
|
97
|
+
Recoverable : ~12.4 KB
|
|
98
|
+
========================================
|
|
99
|
+
|
|
100
|
+
┌───────────────────────────┬───────┬──────────────────┐
|
|
101
|
+
│ Check │ Found │ Output File │
|
|
102
|
+
├───────────────────────────┼───────┼──────────────────┤
|
|
103
|
+
│ Circular Dependencies │ 2 │ circular.txt │
|
|
104
|
+
│ Duplicate Clusters │ 3 │ dupes.md │
|
|
105
|
+
│ Unused Packages │ 4 │ deps.md │
|
|
106
|
+
│ Unused Assets │ 5 │ assets.md │
|
|
107
|
+
└───────────────────────────┴───────┴──────────────────┘
|
|
101
108
|
```
|
|
102
109
|
|
|
103
110
|
---
|
|
@@ -184,7 +191,7 @@ prunify builds a directed import graph (with `tsconfig.json` path aliases) and a
|
|
|
184
191
|
|
|
185
192
|
1. **Safe to delete** — No other project file imports this file, and it is not treated as an app entry (e.g. Next.js `pages/` / `app/` routes) or a known framework/config file (`next.config.*`, `middleware.*`, `tailwind.config.*`, etc.).
|
|
186
193
|
2. **Transitively dead** — The file is only imported by files that are already dead; removing the safe-to-delete roots would leave it orphaned.
|
|
187
|
-
3. **Dead exports** — The file is still used, but specific named exports are never imported by name elsewhere (namespace imports count as “all exports used”).
|
|
194
|
+
3. **Dead exports** — The file is still used, but specific named exports are never imported by name elsewhere (namespace imports count as “all exports used”). Files under **`pages/`** / **`src/pages/`**, route trees like **`src/routes/`**, **`src/views/`**, **`src/screens/`**, and Next.js App Router entry files (**`page.tsx`**, **`layout.tsx`**, **`route.ts`**, etc. under **`app/`** / **`src/app/`**) are **excluded** — the framework consumes those exports without normal `import` edges.
|
|
188
195
|
|
|
189
196
|
**Why not “reachable from entry” only?** A pure reachability scan from entry points can mark heavily shared modules as dead if any link in the chain fails to resolve. The importer-based model matches the usual meaning of “nothing uses this file.”
|
|
190
197
|
|
package/dist/cli.cjs
CHANGED
|
@@ -238,6 +238,83 @@ 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();
|
|
@@ -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) {
|
|
@@ -1351,6 +1430,19 @@ function countDeadExportsInReport(content) {
|
|
|
1351
1430
|
}
|
|
1352
1431
|
return n;
|
|
1353
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
|
+
}
|
|
1354
1446
|
function parseAssetsReportFile(reportPath, rootDir) {
|
|
1355
1447
|
if (!import_node_fs9.default.existsSync(reportPath)) return [];
|
|
1356
1448
|
const content = import_node_fs9.default.readFileSync(reportPath, "utf-8");
|
|
@@ -1479,23 +1571,35 @@ async function main(opts) {
|
|
|
1479
1571
|
entryPoints = opts.entry ? [import_node_path10.default.resolve(opts.entry)] : findEntryPoints(rootDir, packageJson);
|
|
1480
1572
|
}
|
|
1481
1573
|
console.log();
|
|
1482
|
-
let
|
|
1574
|
+
let deadSafeCount = 0;
|
|
1575
|
+
let deadTransitiveCount = 0;
|
|
1576
|
+
let deadExportsCount = 0;
|
|
1577
|
+
let deadRecoverDisplay = "~0.0 KB";
|
|
1483
1578
|
let dupeCount = 0;
|
|
1484
1579
|
let unusedPkgCount = 0;
|
|
1485
1580
|
let circularCount = 0;
|
|
1486
1581
|
let unusedAssetCount = 0;
|
|
1487
|
-
let deadReportFile = "";
|
|
1488
1582
|
let dupesReportFile = "";
|
|
1489
1583
|
let depsReportFile = "";
|
|
1490
1584
|
let circularReportFile = "";
|
|
1491
1585
|
let assetsReportFile = "";
|
|
1492
1586
|
if (loadDeadFromCache) {
|
|
1493
|
-
deadReportFile = "dead-code.txt";
|
|
1494
1587
|
try {
|
|
1495
1588
|
const raw = import_node_fs10.default.readFileSync(deadReportPath, "utf-8");
|
|
1496
|
-
|
|
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
|
+
}
|
|
1497
1601
|
} catch {
|
|
1498
|
-
|
|
1602
|
+
deadSafeCount = deadFilePaths.length;
|
|
1499
1603
|
}
|
|
1500
1604
|
}
|
|
1501
1605
|
if (loadAssetsFromCache) {
|
|
@@ -1505,19 +1609,22 @@ async function main(opts) {
|
|
|
1505
1609
|
if (runDeadCode2) {
|
|
1506
1610
|
const spinner = createSpinner(import_chalk7.default.cyan("Analysing dead code\u2026"));
|
|
1507
1611
|
const result = runDeadCodeModule(project, graph, entryPoints, rootDir);
|
|
1508
|
-
deadFileCount = result.safeToDelete.length + result.transitivelyDead.length + result.deadExports.length;
|
|
1509
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`;
|
|
1510
1618
|
spinner.succeed(
|
|
1511
1619
|
import_chalk7.default.green(
|
|
1512
1620
|
`Dead code analysis complete \u2014 ${result.safeToDelete.length} safe to delete, ${result.transitivelyDead.length} transitively dead, ${result.deadExports.length} dead export(s)`
|
|
1513
1621
|
)
|
|
1514
1622
|
);
|
|
1515
1623
|
if (result.report) {
|
|
1516
|
-
deadReportFile = "dead-code.txt";
|
|
1517
1624
|
const banner = `prunify ${PKG_VERSION} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1518
1625
|
|
|
1519
1626
|
`;
|
|
1520
|
-
writeReport(reportsDir,
|
|
1627
|
+
writeReport(reportsDir, "dead-code.txt", banner + result.report);
|
|
1521
1628
|
}
|
|
1522
1629
|
}
|
|
1523
1630
|
if (modules.includes("dupes")) {
|
|
@@ -1582,13 +1689,25 @@ async function main(opts) {
|
|
|
1582
1689
|
console.log();
|
|
1583
1690
|
console.log(import_chalk7.default.bold("Summary"));
|
|
1584
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
|
+
}
|
|
1585
1706
|
const table = new import_cli_table36.default({
|
|
1586
1707
|
head: [import_chalk7.default.bold("Check"), import_chalk7.default.bold("Found"), import_chalk7.default.bold("Output File")],
|
|
1587
1708
|
style: { head: [], border: [] }
|
|
1588
1709
|
});
|
|
1589
|
-
const fmt = (n) => n > 0 ? import_chalk7.default.yellow(String(n)) : import_chalk7.default.green("0");
|
|
1590
1710
|
table.push(
|
|
1591
|
-
["Dead Code (files + exports)", fmt(deadFileCount), deadReportFile || "\u2014"],
|
|
1592
1711
|
["Circular Dependencies", fmt(circularCount), circularReportFile || "\u2014"],
|
|
1593
1712
|
["Duplicate Clusters", fmt(dupeCount), dupesReportFile || "\u2014"],
|
|
1594
1713
|
["Unused Packages", fmt(unusedPkgCount), depsReportFile || "\u2014"],
|
|
@@ -1665,7 +1784,8 @@ async function main(opts) {
|
|
|
1665
1784
|
console.log(import_chalk7.default.dim(" --delete-assets: no unused assets in report (nothing to delete)."));
|
|
1666
1785
|
}
|
|
1667
1786
|
if (opts.ci) {
|
|
1668
|
-
const
|
|
1787
|
+
const deadIssueCount = deadSafeCount + deadTransitiveCount + deadExportsCount;
|
|
1788
|
+
const hasIssues = deadIssueCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0;
|
|
1669
1789
|
if (hasIssues) process.exit(1);
|
|
1670
1790
|
}
|
|
1671
1791
|
}
|