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 CHANGED
@@ -89,15 +89,22 @@ npx prunify --ci
89
89
 
90
90
  Summary
91
91
 
92
- ┌─────────────────────────────┬───────┬──────────────────┐
93
- Check │ Found │ Output File │
94
- ├─────────────────────────────┼───────┼──────────────────┤
95
- Dead Code (files + exports) │ 7 │ dead-code.txt │
96
- Circular Dependencies 2 │ circular.txt │
97
- Duplicate Clusters │ 3 │ dupes.md │
98
- │ Unused Packages │ 4 │ deps.md │
99
- │ Unused Assets │ 5 │ assets.md │
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 deadFileCount = 0;
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
- deadFileCount = deadFilePaths.length + countDeadExportsInReport(raw);
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
- deadFileCount = deadFilePaths.length;
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, deadReportFile, banner + result.report);
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 hasIssues = deadFileCount > 0 || dupeCount > 0 || unusedPkgCount > 0 || circularCount > 0 || unusedAssetCount > 0;
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
  }