pruny 1.22.0 → 1.23.0

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.
Files changed (2) hide show
  1. package/dist/index.js +299 -302
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9791,20 +9791,30 @@ async function scan(config) {
9791
9791
  "apps/**/app/api/**/route.{ts,tsx,js,jsx}",
9792
9792
  "packages/**/app/api/**/route.{ts,tsx,js,jsx}"
9793
9793
  ];
9794
+ let scanCwd = cwd;
9795
+ let activeNextPatterns = nextPatterns;
9796
+ if (config.appSpecificScan) {
9797
+ scanCwd = config.appSpecificScan.appDir;
9798
+ activeNextPatterns = [
9799
+ "app/api/**/route.{ts,tsx,js,jsx}",
9800
+ "src/app/api/**/route.{ts,tsx,js,jsx}"
9801
+ ];
9802
+ }
9794
9803
  if (config.extraRoutePatterns) {
9795
- nextPatterns.push(...config.extraRoutePatterns);
9804
+ activeNextPatterns.push(...config.extraRoutePatterns);
9796
9805
  }
9797
- const nextFiles = await import_fast_glob4.default(nextPatterns, {
9798
- cwd,
9806
+ const nextFiles = await import_fast_glob4.default(activeNextPatterns, {
9807
+ cwd: scanCwd,
9799
9808
  ignore: config.ignore.folders
9800
9809
  });
9801
9810
  const nextRoutes = nextFiles.map((file) => {
9802
- const content = readFileSync4(join4(cwd, file), "utf-8");
9811
+ const fullPath = join4(scanCwd, file);
9812
+ const content = readFileSync4(fullPath, "utf-8");
9803
9813
  const { methods, methodLines } = extractExportedMethods(content);
9804
9814
  return {
9805
9815
  type: "nextjs",
9806
9816
  path: extractRoutePath(file),
9807
- filePath: file,
9817
+ filePath: fullPath.replace(config.appSpecificScan ? config.appSpecificScan.rootDir + "/" : cwd + "/", ""),
9808
9818
  used: false,
9809
9819
  references: [],
9810
9820
  methods,
@@ -9814,12 +9824,14 @@ async function scan(config) {
9814
9824
  });
9815
9825
  const nestPatterns = ["**/*.controller.ts"];
9816
9826
  const nestFiles = await import_fast_glob4.default(nestPatterns, {
9817
- cwd,
9827
+ cwd: scanCwd,
9818
9828
  ignore: config.ignore.folders
9819
9829
  });
9820
9830
  const nestRoutes = nestFiles.flatMap((file) => {
9821
- const content = readFileSync4(join4(cwd, file), "utf-8");
9822
- return extractNestRoutes(file, content, config.nestGlobalPrefix);
9831
+ const fullPath = join4(scanCwd, file);
9832
+ const content = readFileSync4(fullPath, "utf-8");
9833
+ const relativePathFromRoot = fullPath.replace(config.appSpecificScan ? config.appSpecificScan.rootDir + "/" : cwd + "/", "");
9834
+ return extractNestRoutes(relativePathFromRoot, content, config.nestGlobalPrefix);
9823
9835
  });
9824
9836
  const routes = [...nextRoutes, ...nestRoutes];
9825
9837
  const cronPaths = getVercelCronPaths(cwd);
@@ -9831,15 +9843,16 @@ async function scan(config) {
9831
9843
  route.unusedMethods = [];
9832
9844
  }
9833
9845
  }
9846
+ const referenceScanCwd = config.appSpecificScan ? config.appSpecificScan.rootDir : cwd;
9834
9847
  const extGlob = `**/*{${config.extensions.join(",")}}`;
9835
9848
  const sourceFiles = await import_fast_glob4.default(extGlob, {
9836
- cwd,
9849
+ cwd: referenceScanCwd,
9837
9850
  ignore: [...config.ignore.folders, ...config.ignore.files]
9838
9851
  });
9839
9852
  const allReferences = [];
9840
9853
  const fileReferences = new Map;
9841
9854
  for (const file of sourceFiles) {
9842
- const filePath = join4(cwd, file);
9855
+ const filePath = join4(referenceScanCwd, file);
9843
9856
  try {
9844
9857
  const content = readFileSync4(filePath, "utf-8");
9845
9858
  const refs = extractApiReferences(content);
@@ -10248,327 +10261,265 @@ program2.command("init").description("Create a default pruny.config.json file").
10248
10261
  });
10249
10262
  program2.action(async (options) => {
10250
10263
  const startTime = Date.now();
10251
- const config = loadConfig({
10252
- dir: options.dir,
10253
- config: options.config,
10254
- excludePublic: !options.public
10255
- });
10256
- const absoluteDir = config.dir.startsWith("/") ? config.dir : join8(process.cwd(), config.dir);
10257
- config.dir = absoluteDir;
10258
- if (options.verbose) {
10259
- console.log("");
10260
- }
10261
- console.log(source_default.bold(`
10264
+ try {
10265
+ const config = loadConfig({
10266
+ dir: options.dir,
10267
+ config: options.config,
10268
+ excludePublic: !options.public
10269
+ });
10270
+ const absoluteDir = config.dir.startsWith("/") ? config.dir : join8(process.cwd(), config.dir);
10271
+ config.dir = absoluteDir;
10272
+ if (options.verbose)
10273
+ console.log("");
10274
+ console.log(source_default.bold(`
10262
10275
  \uD83D\uDD0D Scanning for unused API routes...
10263
10276
  `));
10264
- try {
10265
10277
  let result = await scan(config);
10266
- const getAppName = (filePath) => {
10267
- if (filePath.startsWith("apps/"))
10268
- return filePath.split("/").slice(0, 2).join("/");
10269
- if (filePath.startsWith("packages/"))
10270
- return filePath.split("/").slice(0, 2).join("/");
10271
- return "Root";
10272
- };
10278
+ logScanStats(result);
10273
10279
  if (options.filter) {
10274
- const filter2 = options.filter.toLowerCase();
10275
- console.log(source_default.blue(`
10276
- \uD83D\uDD0D Filtering results by "${filter2}"...
10277
- `));
10278
- const matchesFilter = (path2) => {
10279
- const lowerPath = path2.toLowerCase();
10280
- const appName = getAppName(path2).toLowerCase();
10281
- if (appName.includes(filter2))
10282
- return true;
10283
- const segments = lowerPath.split("/");
10284
- for (const segment of segments) {
10285
- if (segment === filter2)
10286
- return true;
10287
- const withoutExt = segment.replace(/\.[^.]+$/, "");
10288
- if (withoutExt === filter2)
10289
- return true;
10290
- }
10291
- return lowerPath.includes(filter2);
10292
- };
10293
- result.routes = result.routes.filter((r) => matchesFilter(r.filePath));
10294
- if (result.publicAssets) {
10295
- result.publicAssets.assets = result.publicAssets.assets.filter((a) => matchesFilter(a.path));
10296
- result.publicAssets.total = result.publicAssets.assets.length;
10297
- result.publicAssets.used = result.publicAssets.assets.filter((a) => a.used).length;
10298
- result.publicAssets.unused = result.publicAssets.assets.filter((a) => !a.used).length;
10299
- }
10300
- if (result.unusedFiles) {
10301
- result.unusedFiles.files = result.unusedFiles.files.filter((f) => matchesFilter(f.path));
10302
- result.unusedFiles.total = result.unusedFiles.files.length;
10303
- result.unusedFiles.unused = result.unusedFiles.files.length;
10304
- }
10305
- if (result.unusedExports) {
10306
- result.unusedExports.exports = result.unusedExports.exports.filter((e) => matchesFilter(e.file));
10307
- result.unusedExports.total = result.unusedExports.exports.length;
10308
- result.unusedExports.unused = result.unusedExports.exports.length;
10309
- }
10280
+ filterResults(result, options.filter);
10310
10281
  }
10311
10282
  if (options.json) {
10312
10283
  console.log(JSON.stringify(result, null, 2));
10313
10284
  return;
10314
10285
  }
10315
- const partiallyUnusedRoutes = result.routes.filter((r) => r.used && r.unusedMethods.length > 0);
10316
- if (partiallyUnusedRoutes.length > 0) {
10317
- console.log(source_default.yellow.bold(`⚠️ Partially Unused API Routes:
10318
- `));
10319
- for (const route of partiallyUnusedRoutes) {
10320
- console.log(source_default.yellow(` ${route.path}`));
10321
- console.log(source_default.red(` ❌ Unused: ${route.unusedMethods.join(", ")}`));
10322
- console.log(source_default.dim(` → ${route.filePath}`));
10323
- }
10324
- console.log("");
10325
- }
10326
- const unusedRoutes = result.routes.filter((r) => !r.used);
10327
- if (unusedRoutes.length > 0) {
10328
- console.log(source_default.red.bold(`❌ Unused API Routes (Fully Unused):
10329
- `));
10330
- for (const route of unusedRoutes) {
10331
- const methods = route.methods.length > 0 ? ` (${route.methods.join(", ")})` : "";
10332
- console.log(source_default.red(` ${route.path}${source_default.dim(methods)}`));
10333
- console.log(source_default.dim(` → ${route.filePath}`));
10334
- }
10335
- console.log("");
10336
- }
10337
- if (result.publicAssets) {
10338
- const unusedAssets = result.publicAssets.assets.filter((a) => !a.used);
10339
- if (unusedAssets.length > 0) {
10340
- console.log(source_default.red.bold(`\uD83D\uDDBC️ Unused Public Assets:
10341
- `));
10342
- for (const asset of unusedAssets) {
10343
- console.log(source_default.red(` ${asset.relativePath}`));
10344
- console.log(source_default.dim(` → ${asset.path}`));
10345
- }
10346
- console.log("");
10347
- }
10348
- }
10349
- if (result.unusedFiles && result.unusedFiles.files.length > 0) {
10350
- console.log(source_default.red.bold(`\uD83D\uDCC4 Unused Source Files:
10351
- `));
10352
- for (const file of result.unusedFiles.files) {
10353
- const sizeKb = (file.size / 1024).toFixed(1);
10354
- console.log(source_default.red(` ${file.path} ${source_default.dim(`(${sizeKb} KB)`)}`));
10355
- }
10356
- console.log("");
10357
- }
10358
- if (result.unusedExports && result.unusedExports.exports.length > 0) {
10359
- console.log(source_default.red.bold(`\uD83D\uDD17 Unused Named Exports/Methods:
10286
+ if (options.fix) {
10287
+ await handleFixes(result, config, options);
10288
+ } else if (hasUnusedItems(result)) {
10289
+ console.log(source_default.dim(`\uD83D\uDCA1 Run with --fix to automatically clean up unused routes, files, and exports.
10360
10290
  `));
10361
- for (const exp of result.unusedExports.exports) {
10362
- console.log(source_default.red(` ${exp.name}`));
10363
- console.log(source_default.dim(` → ${exp.file}:${exp.line}`));
10364
- }
10365
- console.log("");
10366
10291
  }
10367
- if (unusedRoutes.length === 0 && partiallyUnusedRoutes.length === 0 && (!result.publicAssets || result.publicAssets.unused === 0)) {
10368
- console.log(source_default.green(`✅ Everything is used! Clean as a whistle.
10292
+ printSummaryTable(result);
10293
+ } catch (err) {
10294
+ console.error(source_default.red("Error scanning:"), err);
10295
+ process.exit(1);
10296
+ }
10297
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
10298
+ console.log(source_default.dim(`
10299
+ ⏱️ Completed in ${elapsed}s`));
10300
+ });
10301
+ program2.parse();
10302
+ function logScanStats(result) {
10303
+ console.log(source_default.blue.bold("\uD83D\uDCCA Scan Statistics:"));
10304
+ console.log(source_default.blue(` • API Routes: ${result.total}`));
10305
+ if (result.publicAssets) {
10306
+ console.log(source_default.blue(` • Public Assets: ${result.publicAssets.total}`));
10307
+ }
10308
+ if (result.unusedFiles) {
10309
+ console.log(source_default.blue(` • Source Files: ${result.unusedFiles.total}`));
10310
+ }
10311
+ if (result.unusedExports) {
10312
+ console.log(source_default.blue(` • Exported Items: ${result.unusedExports.total}`));
10313
+ }
10314
+ console.log("");
10315
+ }
10316
+ function filterResults(result, filterPattern) {
10317
+ const filter2 = filterPattern.toLowerCase();
10318
+ console.log(source_default.blue(`\uD83D\uDD0D Filtering results by "${filter2}"...
10369
10319
  `));
10320
+ const getAppName = (filePath) => {
10321
+ if (filePath.startsWith("apps/"))
10322
+ return filePath.split("/").slice(0, 2).join("/");
10323
+ if (filePath.startsWith("packages/"))
10324
+ return filePath.split("/").slice(0, 2).join("/");
10325
+ return "Root";
10326
+ };
10327
+ const matchesFilter = (path2) => {
10328
+ const lowerPath = path2.toLowerCase();
10329
+ const appName = getAppName(path2).toLowerCase();
10330
+ if (appName.includes(filter2))
10331
+ return true;
10332
+ const segments = lowerPath.split("/");
10333
+ for (const segment of segments) {
10334
+ if (segment === filter2)
10335
+ return true;
10336
+ const withoutExt = segment.replace(/\.[^.]+$/, "");
10337
+ if (withoutExt === filter2)
10338
+ return true;
10370
10339
  }
10371
- if (options.fix) {
10372
- let fixedSomething = false;
10373
- if (unusedRoutes.length > 0) {
10374
- console.log(source_default.yellow.bold(`\uD83D\uDDD1️ Deleting unused routes...
10340
+ return lowerPath.includes(filter2);
10341
+ };
10342
+ result.routes = result.routes.filter((r) => matchesFilter(r.filePath));
10343
+ if (result.publicAssets) {
10344
+ result.publicAssets.assets = result.publicAssets.assets.filter((a) => matchesFilter(a.path));
10345
+ result.publicAssets.total = result.publicAssets.assets.length;
10346
+ result.publicAssets.used = result.publicAssets.assets.filter((a) => a.used).length;
10347
+ result.publicAssets.unused = result.publicAssets.assets.filter((a) => !a.used).length;
10348
+ }
10349
+ if (result.unusedFiles) {
10350
+ result.unusedFiles.files = result.unusedFiles.files.filter((f) => matchesFilter(f.path));
10351
+ result.unusedFiles.total = result.unusedFiles.files.length;
10352
+ result.unusedFiles.unused = result.unusedFiles.files.length;
10353
+ }
10354
+ if (result.unusedExports) {
10355
+ result.unusedExports.exports = result.unusedExports.exports.filter((e) => matchesFilter(e.file));
10356
+ result.unusedExports.total = result.unusedExports.exports.length;
10357
+ result.unusedExports.unused = result.unusedExports.exports.length;
10358
+ }
10359
+ result.total = result.routes.length;
10360
+ result.used = result.routes.filter((r) => r.used).length;
10361
+ result.unused = result.routes.filter((r) => !r.used).length;
10362
+ }
10363
+ function hasUnusedItems(result) {
10364
+ const unusedRoutes = result.routes.filter((r) => !r.used).length;
10365
+ const partialRoutes = result.routes.filter((r) => r.used && r.unusedMethods.length > 0).length;
10366
+ const unusedAssets = result.publicAssets ? result.publicAssets.unused : 0;
10367
+ const unusedFiles = result.unusedFiles ? result.unusedFiles.unused : 0;
10368
+ const unusedExports = result.unusedExports ? result.unusedExports.unused : 0;
10369
+ return unusedRoutes > 0 || partialRoutes > 0 || unusedAssets > 0 || unusedFiles > 0 || unusedExports > 0;
10370
+ }
10371
+ async function handleFixes(result, config, options) {
10372
+ let fixedSomething = false;
10373
+ const unusedRoutes = result.routes.filter((r) => !r.used);
10374
+ if (unusedRoutes.length > 0) {
10375
+ console.log(source_default.yellow.bold(`\uD83D\uDDD1️ Deleting unused routes...
10375
10376
  `));
10376
- const routesByFile = new Map;
10377
- for (const r of unusedRoutes) {
10378
- const list = routesByFile.get(r.filePath) || [];
10379
- list.push(r);
10380
- routesByFile.set(r.filePath, list);
10381
- }
10382
- for (const [filePath, fileRoutes] of routesByFile) {
10383
- const fullPath = join8(config.dir, filePath);
10384
- if (!existsSync7(fullPath))
10385
- continue;
10386
- const route = fileRoutes[0];
10387
- const routeDir = dirname4(fullPath);
10388
- try {
10389
- if (route.type === "nextjs") {
10390
- if (filePath.includes("app/api") || filePath.includes("pages/api")) {
10391
- rmSync(routeDir, { recursive: true, force: true });
10392
- console.log(source_default.red(` Deleted Folder: ${routeDir}`));
10393
- } else {
10394
- rmSync(fullPath, { force: true });
10395
- console.log(source_default.red(` Deleted File: ${filePath}`));
10396
- }
10397
- fixedSomething = true;
10398
- } else if (route.type === "nestjs") {
10399
- const isInternallyUnused = result.unusedFiles?.files.some((f) => f.path === filePath);
10400
- if (isInternallyUnused || filePath.includes("api/")) {
10401
- rmSync(fullPath, { force: true });
10402
- console.log(source_default.red(` Deleted File: ${filePath}`));
10403
- fixedSomething = true;
10404
- } else {
10405
- console.log(source_default.yellow(` Skipped File Deletion (internally used): ${filePath}`));
10406
- const allMethodsToPrune = [];
10407
- for (const r of fileRoutes) {
10408
- for (const m of r.unusedMethods) {
10409
- if (r.methodLines[m] !== undefined) {
10410
- allMethodsToPrune.push({ method: m, line: r.methodLines[m] });
10411
- }
10412
- }
10413
- }
10414
- allMethodsToPrune.sort((a, b) => b.line - a.line);
10415
- for (const { method, line } of allMethodsToPrune) {
10416
- if (removeMethodFromRoute(config.dir, filePath, method, line)) {
10417
- console.log(source_default.green(` Fixed: Removed ${method} from ${filePath}`));
10418
- fixedSomething = true;
10419
- }
10377
+ const routesByFile = new Map;
10378
+ for (const r of unusedRoutes) {
10379
+ const list = routesByFile.get(r.filePath) || [];
10380
+ list.push(r);
10381
+ routesByFile.set(r.filePath, list);
10382
+ }
10383
+ for (const [filePath, fileRoutes] of routesByFile) {
10384
+ const fullPath = join8(config.dir, filePath);
10385
+ if (!existsSync7(fullPath))
10386
+ continue;
10387
+ const route = fileRoutes[0];
10388
+ const routeDir = dirname4(fullPath);
10389
+ try {
10390
+ if (route.type === "nextjs") {
10391
+ if (filePath.includes("app/api") || filePath.includes("pages/api")) {
10392
+ rmSync(routeDir, { recursive: true, force: true });
10393
+ console.log(source_default.red(` Deleted Folder: ${routeDir}`));
10394
+ } else {
10395
+ rmSync(fullPath, { force: true });
10396
+ console.log(source_default.red(` Deleted File: ${filePath}`));
10397
+ }
10398
+ fixedSomething = true;
10399
+ } else if (route.type === "nestjs") {
10400
+ const isInternallyUnused = result.unusedFiles?.files.some((f) => f.path === filePath);
10401
+ if (isInternallyUnused || filePath.includes("api/")) {
10402
+ rmSync(fullPath, { force: true });
10403
+ console.log(source_default.red(` Deleted File: ${filePath}`));
10404
+ fixedSomething = true;
10405
+ } else {
10406
+ console.log(source_default.yellow(` Skipped File Deletion (internally used): ${filePath}`));
10407
+ const allMethodsToPrune = [];
10408
+ for (const r of fileRoutes) {
10409
+ for (const m of r.unusedMethods) {
10410
+ if (r.methodLines[m] !== undefined) {
10411
+ allMethodsToPrune.push({ method: m, line: r.methodLines[m] });
10420
10412
  }
10421
10413
  }
10422
- } else {
10423
- rmSync(fullPath, { force: true });
10424
- console.log(source_default.red(` Deleted File: ${filePath}`));
10425
- fixedSomething = true;
10426
10414
  }
10427
- for (const r of fileRoutes) {
10428
- const idx = result.routes.indexOf(r);
10429
- if (idx !== -1)
10430
- result.routes.splice(idx, 1);
10415
+ allMethodsToPrune.sort((a, b) => b.line - a.line);
10416
+ for (const { method, line } of allMethodsToPrune) {
10417
+ if (removeMethodFromRoute(config.dir, filePath, method, line)) {
10418
+ console.log(source_default.green(` Fixed: Removed ${method} from ${filePath}`));
10419
+ fixedSomething = true;
10420
+ }
10431
10421
  }
10432
- } catch (err) {
10433
- console.log(source_default.yellow(` Failed to fix: ${filePath}`));
10434
10422
  }
10423
+ } else {
10424
+ rmSync(fullPath, { force: true });
10425
+ console.log(source_default.red(` Deleted File: ${filePath}`));
10426
+ fixedSomething = true;
10435
10427
  }
10436
- console.log("");
10437
- }
10438
- const partiallyRoutes = result.routes.filter((r) => r.used && r.unusedMethods && r.unusedMethods.length > 0);
10439
- if (partiallyRoutes.length > 0) {
10440
- console.log(source_default.yellow.bold(`\uD83D\uDD27 Fixing partially unused routes...
10441
- `));
10442
- for (const route of partiallyRoutes) {
10443
- const sortedMethods = [...route.unusedMethods].filter((m) => route.methodLines[m] !== undefined).sort((a, b) => route.methodLines[b] - route.methodLines[a]);
10444
- let fixedCount = 0;
10445
- for (const method of sortedMethods) {
10446
- const lineNum = route.methodLines[method];
10447
- if (removeMethodFromRoute(config.dir, route.filePath, method, lineNum)) {
10448
- console.log(source_default.green(` Fixed: Removed ${method} from ${route.path}`));
10449
- fixedCount++;
10450
- fixedSomething = true;
10451
- }
10452
- }
10453
- if (fixedCount === route.methods.length) {
10454
- const idx = result.routes.indexOf(route);
10455
- if (idx !== -1)
10456
- result.routes.splice(idx, 1);
10457
- } else {
10458
- route.unusedMethods = route.unusedMethods.filter((m) => !sortedMethods.includes(m));
10459
- }
10428
+ for (const r of fileRoutes) {
10429
+ const idx = result.routes.indexOf(r);
10430
+ if (idx !== -1)
10431
+ result.routes.splice(idx, 1);
10460
10432
  }
10461
- console.log("");
10433
+ } catch (err) {
10434
+ console.log(source_default.yellow(` Failed to fix: ${filePath}`));
10462
10435
  }
10463
- if (result.unusedFiles && result.unusedFiles.files.length > 0) {
10464
- console.log(source_default.yellow.bold(`\uD83D\uDDD1️ Deleting unused source files...
10436
+ }
10437
+ console.log("");
10438
+ }
10439
+ const partiallyRoutes = result.routes.filter((r) => r.used && r.unusedMethods && r.unusedMethods.length > 0);
10440
+ if (partiallyRoutes.length > 0) {
10441
+ console.log(source_default.yellow.bold(`\uD83D\uDD27 Fixing partially unused routes...
10465
10442
  `));
10466
- for (const file of result.unusedFiles.files) {
10467
- try {
10468
- const fullPath = join8(config.dir, file.path);
10469
- if (!existsSync7(fullPath))
10470
- continue;
10471
- rmSync(fullPath, { force: true });
10472
- console.log(source_default.red(` Deleted: ${file.path}`));
10473
- fixedSomething = true;
10474
- } catch (_err) {
10475
- console.log(source_default.yellow(` Failed to delete: ${file.path}`));
10476
- }
10477
- }
10478
- result.unusedFiles.files = [];
10479
- result.unusedFiles.unused = 0;
10480
- console.log("");
10481
- }
10482
- if (result.unusedExports && result.unusedExports.exports.length > 0) {
10483
- fixedSomething = await fixUnusedExports(result, config) || fixedSomething;
10443
+ for (const route of partiallyRoutes) {
10444
+ const sortedMethods = [...route.unusedMethods].filter((m) => route.methodLines[m] !== undefined).sort((a, b) => route.methodLines[b] - route.methodLines[a]);
10445
+ let fixedCount = 0;
10446
+ for (const method of sortedMethods) {
10447
+ const lineNum = route.methodLines[method];
10448
+ if (removeMethodFromRoute(config.dir, route.filePath, method, lineNum)) {
10449
+ console.log(source_default.green(` Fixed: Removed ${method} from ${route.path}`));
10450
+ fixedCount++;
10451
+ fixedSomething = true;
10452
+ }
10453
+ }
10454
+ if (fixedCount === route.methods.length) {
10455
+ const idx = result.routes.indexOf(route);
10456
+ if (idx !== -1)
10457
+ result.routes.splice(idx, 1);
10458
+ } else {
10459
+ route.unusedMethods = route.unusedMethods.filter((m) => !sortedMethods.includes(m));
10484
10460
  }
10485
- if (fixedSomething) {
10486
- console.log(source_default.cyan.bold(`
10487
- \uD83D\uDD04 Checking for cascading dead code (newly unused implementation)...`));
10488
- const secondPass = await scanUnusedExports(config);
10489
- if (options.filter) {
10490
- const filter2 = options.filter.toLowerCase();
10491
- const matchesFilterPass2 = (path2) => {
10492
- const lowerPath = path2.toLowerCase();
10493
- const appName = getAppName(path2).toLowerCase();
10494
- if (appName.includes(filter2))
10495
- return true;
10496
- const segments = lowerPath.split("/");
10497
- for (const segment of segments) {
10498
- if (segment === filter2)
10499
- return true;
10500
- const withoutExt = segment.replace(/\.[^.]+$/, "");
10501
- if (withoutExt === filter2)
10502
- return true;
10503
- }
10504
- return lowerPath.includes(filter2);
10505
- };
10506
- secondPass.exports = secondPass.exports.filter((e) => matchesFilterPass2(e.file));
10507
- secondPass.total = secondPass.exports.length;
10508
- secondPass.unused = secondPass.exports.length;
10509
- }
10510
- if (secondPass.unused > 0) {
10511
- console.log(source_default.yellow(` Found ${secondPass.unused} newly unused items/methods after pruning.
10461
+ }
10462
+ console.log("");
10463
+ }
10464
+ if (result.unusedFiles && result.unusedFiles.files.length > 0) {
10465
+ console.log(source_default.yellow.bold(`\uD83D\uDDD1️ Deleting unused source files...
10512
10466
  `));
10513
- result.unusedExports = secondPass;
10514
- await fixUnusedExports(result, config);
10515
- }
10516
- }
10517
- if (fixedSomething) {
10518
- result.unused = result.routes.filter((r) => !r.used).length;
10519
- result.used = result.routes.filter((r) => r.used).length;
10520
- result.total = result.routes.length;
10467
+ for (const file of result.unusedFiles.files) {
10468
+ try {
10469
+ const fullPath = join8(config.dir, file.path);
10470
+ if (!existsSync7(fullPath))
10471
+ continue;
10472
+ rmSync(fullPath, { force: true });
10473
+ console.log(source_default.red(` Deleted: ${file.path}`));
10474
+ fixedSomething = true;
10475
+ } catch (_err) {
10476
+ console.log(source_default.yellow(` Failed to delete: ${file.path}`));
10521
10477
  }
10522
- } else if (unusedRoutes.length > 0 || result.unusedExports && result.unusedExports.exports.length > 0 || result.unusedFiles && result.unusedFiles.files.length > 0) {
10523
- console.log(source_default.dim(`\uD83D\uDCA1 Run with --fix to automatically clean up unused routes, files, and exports.
10524
- `));
10525
10478
  }
10526
- console.log(source_default.bold(`\uD83D\uDCCA Summary Report
10479
+ result.unusedFiles.files = [];
10480
+ result.unusedFiles.unused = 0;
10481
+ console.log("");
10482
+ }
10483
+ if (result.unusedExports && result.unusedExports.exports.length > 0) {
10484
+ fixedSomething = await fixUnusedExports(result, config) || fixedSomething;
10485
+ }
10486
+ if (fixedSomething) {
10487
+ console.log(source_default.cyan.bold(`
10488
+ \uD83D\uDD04 Checking for cascading dead code (newly unused implementation)...`));
10489
+ const secondPass = await scanUnusedExports(config);
10490
+ if (options.filter) {
10491
+ const filter2 = options.filter.toLowerCase();
10492
+ const getAppName = (filePath) => {
10493
+ if (filePath.startsWith("apps/"))
10494
+ return filePath.split("/").slice(0, 2).join("/");
10495
+ if (filePath.startsWith("packages/"))
10496
+ return filePath.split("/").slice(0, 2).join("/");
10497
+ return "Root";
10498
+ };
10499
+ const matchesFilter = (path2) => {
10500
+ const lowerPath = path2.toLowerCase();
10501
+ const appName = getAppName(path2).toLowerCase();
10502
+ if (appName.includes(filter2))
10503
+ return true;
10504
+ return lowerPath.includes(filter2);
10505
+ };
10506
+ secondPass.exports = secondPass.exports.filter((e) => matchesFilter(e.file));
10507
+ secondPass.total = secondPass.exports.length;
10508
+ secondPass.unused = secondPass.exports.length;
10509
+ }
10510
+ if (secondPass.unused > 0) {
10511
+ console.log(source_default.yellow(` Found ${secondPass.unused} newly unused items/methods after pruning.
10527
10512
  `));
10528
- const summary = [];
10529
- const groupedRoutes = new Map;
10530
- for (const route of result.routes) {
10531
- const keyAppName = getAppName(route.filePath);
10532
- const key = `${keyAppName}::${route.type}`;
10533
- if (!groupedRoutes.has(key)) {
10534
- groupedRoutes.set(key, { type: route.type, app: keyAppName, routes: [] });
10535
- }
10536
- groupedRoutes.get(key).routes.push(route);
10537
- }
10538
- const sortedKeys = Array.from(groupedRoutes.keys()).sort((a, b) => {
10539
- const [appA, typeA] = a.split("::");
10540
- const [appB, typeB] = b.split("::");
10541
- if (typeA !== typeB)
10542
- return typeA === "nextjs" ? -1 : 1;
10543
- return appA.localeCompare(appB);
10544
- });
10545
- for (const key of sortedKeys) {
10546
- const group = groupedRoutes.get(key);
10547
- const typeLabel = group.type === "nextjs" ? "Next.js" : "NestJS";
10548
- summary.push({
10549
- Category: `${typeLabel} (${group.app})`,
10550
- Total: group.routes.length,
10551
- Used: group.routes.filter((r) => r.used).length,
10552
- Unused: group.routes.filter((r) => !r.used).length
10553
- });
10513
+ result.unusedExports = secondPass;
10514
+ await fixUnusedExports(result, config);
10554
10515
  }
10555
- if (summary.length === 0)
10556
- summary.push({ Category: "API Routes", Total: result.total, Used: result.used, Unused: result.unused });
10557
- if (result.publicAssets)
10558
- summary.push({ Category: "Public Assets", Total: result.publicAssets.total, Used: result.publicAssets.used, Unused: result.publicAssets.unused });
10559
- if (result.unusedFiles)
10560
- summary.push({ Category: "Source Files", Total: result.unusedFiles.total, Used: result.unusedFiles.used, Unused: result.unusedFiles.unused });
10561
- if (result.unusedExports)
10562
- summary.push({ Category: "Exported Items", Total: result.unusedExports.total, Used: result.unusedExports.used, Unused: result.unusedExports.unused });
10563
- console.table(summary);
10564
- } catch (_err) {
10565
- console.error(source_default.red("Error scanning:"), _err);
10566
- process.exit(1);
10567
10516
  }
10568
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
10569
- console.log(source_default.dim(`
10570
- ⏱️ Completed in ${elapsed}s`));
10571
- });
10517
+ if (fixedSomething) {
10518
+ result.unused = result.routes.filter((r) => !r.used).length;
10519
+ result.used = result.routes.filter((r) => r.used).length;
10520
+ result.total = result.routes.length;
10521
+ }
10522
+ }
10572
10523
  async function fixUnusedExports(result, config) {
10573
10524
  if (!result.unusedExports || result.unusedExports.exports.length === 0)
10574
10525
  return false;
@@ -10606,4 +10557,50 @@ async function fixUnusedExports(result, config) {
10606
10557
  `));
10607
10558
  return fixedSomething;
10608
10559
  }
10609
- program2.parse();
10560
+ function printSummaryTable(result) {
10561
+ console.log(source_default.bold(`\uD83D\uDCCA Summary Report
10562
+ `));
10563
+ const summary = [];
10564
+ const groupedRoutes = new Map;
10565
+ const getAppName = (filePath) => {
10566
+ if (filePath.startsWith("apps/"))
10567
+ return filePath.split("/").slice(0, 2).join("/");
10568
+ if (filePath.startsWith("packages/"))
10569
+ return filePath.split("/").slice(0, 2).join("/");
10570
+ return "Root";
10571
+ };
10572
+ for (const route of result.routes) {
10573
+ const keyAppName = getAppName(route.filePath);
10574
+ const key = `${keyAppName}::${route.type}`;
10575
+ if (!groupedRoutes.has(key)) {
10576
+ groupedRoutes.set(key, { type: route.type, app: keyAppName, routes: [] });
10577
+ }
10578
+ groupedRoutes.get(key).routes.push(route);
10579
+ }
10580
+ const sortedKeys = Array.from(groupedRoutes.keys()).sort((a, b) => {
10581
+ const [appA, typeA] = a.split("::");
10582
+ const [appB, typeB] = b.split("::");
10583
+ if (typeA !== typeB)
10584
+ return typeA === "nextjs" ? -1 : 1;
10585
+ return appA.localeCompare(appB);
10586
+ });
10587
+ for (const key of sortedKeys) {
10588
+ const group = groupedRoutes.get(key);
10589
+ const typeLabel = group.type === "nextjs" ? "Next.js" : "NestJS";
10590
+ summary.push({
10591
+ Category: `${typeLabel} (${group.app})`,
10592
+ Total: group.routes.length,
10593
+ Used: group.routes.filter((r) => r.used).length,
10594
+ Unused: group.routes.filter((r) => !r.used).length
10595
+ });
10596
+ }
10597
+ if (summary.length === 0)
10598
+ summary.push({ Category: "API Routes", Total: result.total, Used: result.used, Unused: result.unused });
10599
+ if (result.publicAssets)
10600
+ summary.push({ Category: "Public Assets", Total: result.publicAssets.total, Used: result.publicAssets.used, Unused: result.publicAssets.unused });
10601
+ if (result.unusedFiles)
10602
+ summary.push({ Category: "Source Files", Total: result.unusedFiles.total, Used: result.unusedFiles.used, Unused: result.unusedFiles.unused });
10603
+ if (result.unusedExports)
10604
+ summary.push({ Category: "Exported Items", Total: result.unusedExports.total, Used: result.unusedExports.used, Unused: result.unusedExports.unused });
10605
+ console.table(summary);
10606
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "description": "Find and remove unused Next.js API routes & Nest.js Controllers",
5
5
  "type": "module",
6
6
  "files": [