pruny 1.22.0 → 1.24.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 +416 -351
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7631,7 +7631,7 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
7631
7631
  var source_default = chalk;
7632
7632
 
7633
7633
  // src/index.ts
7634
- import { rmSync, existsSync as existsSync7 } from "node:fs";
7634
+ import { rmSync, existsSync as existsSync7, readdirSync, lstatSync } from "node:fs";
7635
7635
  import { dirname as dirname4, join as join8 } from "node:path";
7636
7636
 
7637
7637
  // src/scanner.ts
@@ -9193,12 +9193,16 @@ var import_fast_glob2 = __toESM(require_out4(), 1);
9193
9193
  import { readFileSync as readFileSync2, statSync, existsSync as existsSync2 } from "node:fs";
9194
9194
  import { join as join2, dirname, resolve, relative, sep as sep2 } from "node:path";
9195
9195
  async function scanUnusedFiles(config) {
9196
- const cwd = config.dir;
9196
+ const rootDir = config.appSpecificScan ? config.appSpecificScan.rootDir : config.dir;
9197
+ const searchDir = config.appSpecificScan ? config.appSpecificScan.appDir : config.dir;
9197
9198
  const extensions = config.extensions;
9198
9199
  const extGlob = `**/*{${extensions.join(",")}}`;
9200
+ console.log(`
9201
+ \uD83D\uDD0D Finding source files in: ${searchDir}`);
9199
9202
  const allFiles = await import_fast_glob2.default(extGlob, {
9200
- cwd,
9201
- ignore: [...config.ignore.folders, ...config.ignore.files]
9203
+ cwd: searchDir,
9204
+ ignore: [...config.ignore.folders, ...config.ignore.files],
9205
+ absolute: true
9202
9206
  });
9203
9207
  if (allFiles.length === 0) {
9204
9208
  return { total: 0, used: 0, unused: 0, files: [] };
@@ -9245,8 +9249,9 @@ async function scanUnusedFiles(config) {
9245
9249
  "**/api/index.ts"
9246
9250
  ];
9247
9251
  for (const file of allFiles) {
9252
+ const relPath = relative(searchDir, file);
9248
9253
  const isEntry = entryPatterns.some((pattern) => {
9249
- return minimatch(file, pattern, { dot: true });
9254
+ return minimatch(relPath, pattern, { dot: true });
9250
9255
  });
9251
9256
  if (isEntry)
9252
9257
  entryFiles.add(file);
@@ -9257,9 +9262,9 @@ async function scanUnusedFiles(config) {
9257
9262
  const importRegex = /from\s+['"]([^'"]+)['"]|import\(['"]([^'"]+)['"]\)|require\(['"]([^'"]+)['"]\)/g;
9258
9263
  while (queue.length > 0) {
9259
9264
  const currentFile = queue.shift();
9260
- const currentDir = dirname(join2(cwd, currentFile));
9265
+ const currentDir = dirname(currentFile);
9261
9266
  try {
9262
- const content = readFileSync2(join2(cwd, currentFile), "utf-8");
9267
+ const content = readFileSync2(currentFile, "utf-8");
9263
9268
  let match2;
9264
9269
  importRegex.lastIndex = 0;
9265
9270
  while ((match2 = importRegex.exec(content)) !== null) {
@@ -9268,23 +9273,30 @@ async function scanUnusedFiles(config) {
9268
9273
  continue;
9269
9274
  let resolvedFile = null;
9270
9275
  if (imp.startsWith(".")) {
9271
- resolvedFile = resolveImport(currentDir, imp, extensions, cwd);
9276
+ resolvedFile = resolveImport(currentDir, imp, extensions, rootDir);
9272
9277
  } else if (imp.startsWith("@/") || imp.startsWith("~/")) {
9273
9278
  const aliasPath = imp.substring(2);
9274
- resolvedFile = resolveImport(cwd, aliasPath, extensions, cwd) || resolveImport(join2(cwd, "src"), aliasPath, extensions, cwd) || resolveImport(join2(cwd, "app"), aliasPath, extensions, cwd);
9279
+ resolvedFile = resolveImport(rootDir, aliasPath, extensions, rootDir) || resolveImport(join2(rootDir, "src"), aliasPath, extensions, rootDir) || resolveImport(join2(rootDir, "app"), aliasPath, extensions, rootDir);
9275
9280
  if (!resolvedFile) {
9276
- const pathParts = currentFile.split(/[/\\]/);
9277
- if (pathParts.length >= 2 && (pathParts[0] === "apps" || pathParts[0] === "packages")) {
9278
- const projectRoot = join2(cwd, pathParts[0], pathParts[1]);
9279
- resolvedFile = resolveImport(projectRoot, aliasPath, extensions, cwd) || resolveImport(join2(projectRoot, "src"), aliasPath, extensions, cwd) || resolveImport(join2(projectRoot, "app"), aliasPath, extensions, cwd);
9281
+ const pathParts = currentFile.split(sep2);
9282
+ const appsIndex = pathParts.lastIndexOf("apps");
9283
+ const packagesIndex = pathParts.lastIndexOf("packages");
9284
+ const index = Math.max(appsIndex, packagesIndex);
9285
+ if (index !== -1 && index + 1 < pathParts.length) {
9286
+ const projectRoot = pathParts.slice(0, index + 2).join(sep2);
9287
+ resolvedFile = resolveImport(projectRoot, aliasPath, extensions, rootDir) || resolveImport(join2(projectRoot, "src"), aliasPath, extensions, rootDir) || resolveImport(join2(projectRoot, "app"), aliasPath, extensions, rootDir);
9280
9288
  }
9281
9289
  }
9282
9290
  }
9283
- if (resolvedFile && allFilesSet.has(resolvedFile)) {
9284
- usedFiles.add(resolvedFile);
9285
- if (!visited.has(resolvedFile)) {
9286
- visited.add(resolvedFile);
9287
- queue.push(resolvedFile);
9291
+ if (resolvedFile) {
9292
+ const absoluteResolved = join2(rootDir, resolvedFile);
9293
+ const absoluteTarget = resolve(rootDir, resolvedFile);
9294
+ if (!visited.has(absoluteTarget) && existsSync2(absoluteTarget) && statSync(absoluteTarget).isFile()) {
9295
+ visited.add(absoluteTarget);
9296
+ usedFiles.add(absoluteTarget);
9297
+ queue.push(absoluteTarget);
9298
+ } else {
9299
+ usedFiles.add(absoluteTarget);
9288
9300
  }
9289
9301
  }
9290
9302
  }
@@ -9293,15 +9305,15 @@ async function scanUnusedFiles(config) {
9293
9305
  const unusedResults = [];
9294
9306
  for (const file of allFiles) {
9295
9307
  if (!usedFiles.has(file)) {
9296
- const fullPath = join2(cwd, file);
9308
+ const displayPath = relative(rootDir, file);
9297
9309
  try {
9298
- const stats = statSync(fullPath);
9310
+ const stats = statSync(file);
9299
9311
  unusedResults.push({
9300
- path: file,
9312
+ path: displayPath,
9301
9313
  size: stats.size
9302
9314
  });
9303
9315
  } catch {
9304
- unusedResults.push({ path: file, size: 0 });
9316
+ unusedResults.push({ path: displayPath, size: 0 });
9305
9317
  }
9306
9318
  }
9307
9319
  }
@@ -9337,7 +9349,7 @@ function resolveImport(baseDir, impPath, extensions, rootDir) {
9337
9349
  // src/scanners/unused-exports.ts
9338
9350
  var import_fast_glob3 = __toESM(require_out4(), 1);
9339
9351
  import { readFileSync as readFileSync3 } from "node:fs";
9340
- import { join as join3 } from "node:path";
9352
+ import { join as join3, relative as relative2 } from "node:path";
9341
9353
  import { Worker } from "node:worker_threads";
9342
9354
  import { fileURLToPath } from "node:url";
9343
9355
  import { dirname as dirname2 } from "node:path";
@@ -9440,42 +9452,66 @@ async function scanUnusedExports(config) {
9440
9452
  const cwd = config.dir;
9441
9453
  const extensions = config.extensions;
9442
9454
  const extGlob = `**/*{${extensions.join(",")}}`;
9443
- const allFiles = await import_fast_glob3.default(extGlob, {
9444
- cwd,
9445
- ignore: [...config.ignore.folders, ...config.ignore.files]
9455
+ const candidateCwd = config.appSpecificScan ? config.appSpecificScan.appDir : cwd;
9456
+ const referenceCwd = config.appSpecificScan ? config.appSpecificScan.rootDir : cwd;
9457
+ console.log(`
9458
+ \uD83D\uDD0D Finding export candidates in: ${candidateCwd}`);
9459
+ console.log(` \uD83C\uDF0D Checking usage in global scope: ${referenceCwd}
9460
+ `);
9461
+ const candidateFiles = await import_fast_glob3.default(extGlob, {
9462
+ cwd: candidateCwd,
9463
+ ignore: [...config.ignore.folders, ...config.ignore.files],
9464
+ absolute: true
9446
9465
  });
9447
- if (allFiles.length === 0) {
9466
+ if (candidateFiles.length === 0) {
9448
9467
  return { total: 0, used: 0, unused: 0, exports: [] };
9449
9468
  }
9469
+ const referenceFiles = await import_fast_glob3.default(extGlob, {
9470
+ cwd: referenceCwd,
9471
+ ignore: [...config.ignore.folders, ...config.ignore.files],
9472
+ absolute: true
9473
+ });
9450
9474
  const exportMap = new Map;
9451
9475
  const totalContents = new Map;
9452
9476
  let allExportsCount = 0;
9453
9477
  const inlineExportRegex = /^export\s+(?:async\s+)?(?:const|let|var|function|type|interface|enum|class)\s+([a-zA-Z0-9_$]+)/gm;
9454
9478
  const blockExportRegex = /^export\s*\{([^}]+)\}/gm;
9455
- const USE_WORKERS = allFiles.length >= 500;
9479
+ const USE_WORKERS = referenceFiles.length >= 500;
9456
9480
  const WORKER_COUNT = 2;
9457
9481
  if (USE_WORKERS) {
9458
- console.log(`\uD83D\uDCDD Scanning ${allFiles.length} files for exports (using ${WORKER_COUNT} workers)...`);
9459
- const result = await processFilesInParallel(allFiles, cwd, WORKER_COUNT);
9460
- for (const [file, exports] of result.exportMap.entries()) {
9461
- exportMap.set(file, exports);
9462
- allExportsCount += exports.length;
9463
- }
9482
+ console.log(`\uD83D\uDCDD Scanning ${candidateFiles.length} candidate files for exports & ${referenceFiles.length} files for usage...`);
9483
+ const result = await processFilesInParallel(referenceFiles, referenceCwd, WORKER_COUNT);
9464
9484
  for (const [file, content] of result.contents.entries()) {
9465
9485
  totalContents.set(file, content);
9466
9486
  }
9487
+ const candidateSet = new Set(candidateFiles);
9488
+ for (const [file, exports] of result.exportMap.entries()) {
9489
+ const absoluteFile = file.startsWith("/") ? file : join3(referenceCwd, file);
9490
+ if (candidateSet.has(absoluteFile)) {
9491
+ const displayPath = relative2(config.dir, absoluteFile);
9492
+ const mappedExports = exports.map((e) => ({ ...e, file: displayPath }));
9493
+ exportMap.set(displayPath, mappedExports);
9494
+ allExportsCount += mappedExports.length;
9495
+ }
9496
+ }
9467
9497
  } else {
9468
- console.log(`\uD83D\uDCDD Scanning ${allFiles.length} files for exports...`);
9498
+ console.log(`\uD83D\uDCDD Scanning ${candidateFiles.length} files for exports...`);
9499
+ for (const file of referenceFiles) {
9500
+ try {
9501
+ const content = readFileSync3(file, "utf-8");
9502
+ totalContents.set(file, content);
9503
+ } catch {}
9504
+ }
9469
9505
  let processedFiles = 0;
9470
- for (const file of allFiles) {
9506
+ for (const file of candidateFiles) {
9471
9507
  try {
9472
9508
  processedFiles++;
9473
- if (processedFiles % 10 === 0 || processedFiles === allFiles.length) {
9474
- const percent = Math.round(processedFiles / allFiles.length * 100);
9475
- const shortFile = file.length > 50 ? "..." + file.slice(-47) : file;
9476
- process.stdout.write(`\r Progress: ${processedFiles}/${allFiles.length} (${percent}%) - ${shortFile}${" ".repeat(10)}`);
9509
+ const displayPath = relative2(config.dir, file);
9510
+ if (processedFiles % 10 === 0 || processedFiles === candidateFiles.length) {
9511
+ const percent = Math.round(processedFiles / candidateFiles.length * 100);
9512
+ process.stdout.write(`\r Progress: ${processedFiles}/${candidateFiles.length} (${percent}%)`);
9477
9513
  }
9478
- const content = readFileSync3(join3(cwd, file), "utf-8");
9514
+ const content = totalContents.get(file) || readFileSync3(file, "utf-8");
9479
9515
  totalContents.set(file, content);
9480
9516
  const isService = file.endsWith(".service.ts") || file.endsWith(".service.tsx");
9481
9517
  const lines = content.split(`
@@ -9485,7 +9521,7 @@ async function scanUnusedExports(config) {
9485
9521
  inlineExportRegex.lastIndex = 0;
9486
9522
  let match2;
9487
9523
  while ((match2 = inlineExportRegex.exec(line)) !== null) {
9488
- if (addExport(file, match2[1], i + 1)) {
9524
+ if (addExport(displayPath, match2[1], i + 1)) {
9489
9525
  allExportsCount++;
9490
9526
  }
9491
9527
  }
@@ -9496,7 +9532,7 @@ async function scanUnusedExports(config) {
9496
9532
  return parts[parts.length - 1];
9497
9533
  });
9498
9534
  for (const name of names) {
9499
- if (addExport(file, name, i + 1)) {
9535
+ if (addExport(displayPath, name, i + 1)) {
9500
9536
  allExportsCount++;
9501
9537
  }
9502
9538
  }
@@ -9506,9 +9542,9 @@ async function scanUnusedExports(config) {
9506
9542
  while ((match2 = classMethodRegex.exec(line)) !== null) {
9507
9543
  const name = match2[1];
9508
9544
  if (name && !NEST_LIFECYCLE_METHODS.has(name) && !IGNORED_EXPORT_NAMES.has(name)) {
9509
- const existing = exportMap.get(file)?.find((e) => e.name === name);
9545
+ const existing = exportMap.get(displayPath)?.find((e) => e.name === name);
9510
9546
  if (!existing) {
9511
- if (addExport(file, name, i + 1)) {
9547
+ if (addExport(displayPath, name, i + 1)) {
9512
9548
  allExportsCount++;
9513
9549
  }
9514
9550
  }
@@ -9791,20 +9827,30 @@ async function scan(config) {
9791
9827
  "apps/**/app/api/**/route.{ts,tsx,js,jsx}",
9792
9828
  "packages/**/app/api/**/route.{ts,tsx,js,jsx}"
9793
9829
  ];
9830
+ let scanCwd = cwd;
9831
+ let activeNextPatterns = nextPatterns;
9832
+ if (config.appSpecificScan) {
9833
+ scanCwd = config.appSpecificScan.appDir;
9834
+ activeNextPatterns = [
9835
+ "app/api/**/route.{ts,tsx,js,jsx}",
9836
+ "src/app/api/**/route.{ts,tsx,js,jsx}"
9837
+ ];
9838
+ }
9794
9839
  if (config.extraRoutePatterns) {
9795
- nextPatterns.push(...config.extraRoutePatterns);
9840
+ activeNextPatterns.push(...config.extraRoutePatterns);
9796
9841
  }
9797
- const nextFiles = await import_fast_glob4.default(nextPatterns, {
9798
- cwd,
9842
+ const nextFiles = await import_fast_glob4.default(activeNextPatterns, {
9843
+ cwd: scanCwd,
9799
9844
  ignore: config.ignore.folders
9800
9845
  });
9801
9846
  const nextRoutes = nextFiles.map((file) => {
9802
- const content = readFileSync4(join4(cwd, file), "utf-8");
9847
+ const fullPath = join4(scanCwd, file);
9848
+ const content = readFileSync4(fullPath, "utf-8");
9803
9849
  const { methods, methodLines } = extractExportedMethods(content);
9804
9850
  return {
9805
9851
  type: "nextjs",
9806
9852
  path: extractRoutePath(file),
9807
- filePath: file,
9853
+ filePath: fullPath.replace(config.appSpecificScan ? config.appSpecificScan.rootDir + "/" : cwd + "/", ""),
9808
9854
  used: false,
9809
9855
  references: [],
9810
9856
  methods,
@@ -9814,12 +9860,14 @@ async function scan(config) {
9814
9860
  });
9815
9861
  const nestPatterns = ["**/*.controller.ts"];
9816
9862
  const nestFiles = await import_fast_glob4.default(nestPatterns, {
9817
- cwd,
9863
+ cwd: scanCwd,
9818
9864
  ignore: config.ignore.folders
9819
9865
  });
9820
9866
  const nestRoutes = nestFiles.flatMap((file) => {
9821
- const content = readFileSync4(join4(cwd, file), "utf-8");
9822
- return extractNestRoutes(file, content, config.nestGlobalPrefix);
9867
+ const fullPath = join4(scanCwd, file);
9868
+ const content = readFileSync4(fullPath, "utf-8");
9869
+ const relativePathFromRoot = fullPath.replace(config.appSpecificScan ? config.appSpecificScan.rootDir + "/" : cwd + "/", "");
9870
+ return extractNestRoutes(relativePathFromRoot, content, config.nestGlobalPrefix);
9823
9871
  });
9824
9872
  const routes = [...nextRoutes, ...nestRoutes];
9825
9873
  const cronPaths = getVercelCronPaths(cwd);
@@ -9831,15 +9879,16 @@ async function scan(config) {
9831
9879
  route.unusedMethods = [];
9832
9880
  }
9833
9881
  }
9882
+ const referenceScanCwd = config.appSpecificScan ? config.appSpecificScan.rootDir : cwd;
9834
9883
  const extGlob = `**/*{${config.extensions.join(",")}}`;
9835
9884
  const sourceFiles = await import_fast_glob4.default(extGlob, {
9836
- cwd,
9885
+ cwd: referenceScanCwd,
9837
9886
  ignore: [...config.ignore.folders, ...config.ignore.files]
9838
9887
  });
9839
9888
  const allReferences = [];
9840
9889
  const fileReferences = new Map;
9841
9890
  for (const file of sourceFiles) {
9842
- const filePath = join4(cwd, file);
9891
+ const filePath = join4(referenceScanCwd, file);
9843
9892
  try {
9844
9893
  const content = readFileSync4(filePath, "utf-8");
9845
9894
  const refs = extractApiReferences(content);
@@ -9890,7 +9939,7 @@ async function scan(config) {
9890
9939
  // src/config.ts
9891
9940
  var import_fast_glob5 = __toESM(require_out4(), 1);
9892
9941
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
9893
- import { join as join5, resolve as resolve2, relative as relative2, dirname as dirname3 } from "node:path";
9942
+ import { join as join5, resolve as resolve2, relative as relative3, dirname as dirname3 } from "node:path";
9894
9943
  var DEFAULT_CONFIG = {
9895
9944
  dir: "./",
9896
9945
  ignore: {
@@ -9964,7 +10013,7 @@ function loadConfig(options) {
9964
10013
  const content = readFileSync5(configPath, "utf-8");
9965
10014
  const config = JSON.parse(content);
9966
10015
  const configDir = dirname3(configPath);
9967
- const relDir = relative2(cwd, configDir);
10016
+ const relDir = relative3(cwd, configDir);
9968
10017
  const prefixPattern = (p) => {
9969
10018
  if (p.startsWith("**/") || p.startsWith("/") || !relDir)
9970
10019
  return p;
@@ -10248,327 +10297,297 @@ program2.command("init").description("Create a default pruny.config.json file").
10248
10297
  });
10249
10298
  program2.action(async (options) => {
10250
10299
  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(`
10262
- \uD83D\uDD0D Scanning for unused API routes...
10263
- `));
10264
10300
  try {
10265
- 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
- };
10273
- 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
- }
10310
- }
10311
- if (options.json) {
10312
- console.log(JSON.stringify(result, null, 2));
10313
- return;
10314
- }
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
- }
10301
+ const baseConfig = loadConfig({
10302
+ dir: options.dir,
10303
+ config: options.config,
10304
+ excludePublic: !options.public
10305
+ });
10306
+ const absoluteDir = baseConfig.dir.startsWith("/") ? baseConfig.dir : join8(process.cwd(), baseConfig.dir);
10307
+ baseConfig.dir = absoluteDir;
10308
+ if (options.verbose)
10324
10309
  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):
10310
+ console.log(source_default.bold(`
10311
+ \uD83D\uDD0D Scanning for unused API routes...
10329
10312
  `));
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:
10313
+ const appsDir = join8(absoluteDir, "apps");
10314
+ const isMonorepo = existsSync7(appsDir) && lstatSync(appsDir).isDirectory();
10315
+ const appsToScan = [];
10316
+ if (isMonorepo) {
10317
+ const apps = readdirSync(appsDir);
10318
+ for (const app of apps) {
10319
+ const appPath = join8(appsDir, app);
10320
+ if (lstatSync(appPath).isDirectory()) {
10321
+ appsToScan.push(app);
10322
+ }
10323
+ }
10324
+ console.log(source_default.bold(`
10325
+ \uD83C\uDFE2 Monorepo Detected. Found ${appsToScan.length} apps: ${appsToScan.join(", ")}
10341
10326
  `));
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("");
10327
+ } else {
10328
+ appsToScan.push("root");
10329
+ }
10330
+ for (const appName of appsToScan) {
10331
+ const currentConfig = { ...baseConfig };
10332
+ let appLabel = "Root App";
10333
+ let appDir = absoluteDir;
10334
+ if (isMonorepo) {
10335
+ appLabel = `App: ${appName}`;
10336
+ appDir = join8(absoluteDir, "apps", appName);
10337
+ currentConfig.appSpecificScan = {
10338
+ appDir,
10339
+ rootDir: absoluteDir
10340
+ };
10347
10341
  }
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)`)}`));
10342
+ console.log(source_default.bold.magenta(`
10343
+ \uD83D\uDC49 Scanning ${appLabel}...`));
10344
+ let result = await scan(currentConfig);
10345
+ logScanStats(result, appLabel);
10346
+ if (options.filter) {
10347
+ filterResults(result, options.filter);
10355
10348
  }
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:
10349
+ if (options.json) {
10350
+ console.log(JSON.stringify(result, null, 2));
10351
+ } else {
10352
+ if (options.fix) {
10353
+ await handleFixes(result, currentConfig, options);
10354
+ } else if (hasUnusedItems(result)) {
10355
+ console.log(source_default.dim(`\uD83D\uDCA1 Run with --fix to clean up.
10360
10356
  `));
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}`));
10357
+ }
10358
+ printSummaryTable(result, appLabel);
10364
10359
  }
10365
- console.log("");
10366
10360
  }
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.
10361
+ } catch (err) {
10362
+ console.error(source_default.red("Error scanning:"), err);
10363
+ process.exit(1);
10364
+ }
10365
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
10366
+ console.log(source_default.dim(`
10367
+ ⏱️ Completed in ${elapsed}s`));
10368
+ });
10369
+ program2.parse();
10370
+ function logScanStats(result, context) {
10371
+ console.log(source_default.blue.bold(`\uD83D\uDCCA stats for ${context}:`));
10372
+ console.log(source_default.blue(` • API Routes: ${result.total}`));
10373
+ if (result.publicAssets) {
10374
+ console.log(source_default.blue(` • Public Assets: ${result.publicAssets.total}`));
10375
+ }
10376
+ if (result.unusedFiles) {
10377
+ console.log(source_default.blue(` • Source Files: ${result.unusedFiles.total}`));
10378
+ }
10379
+ if (result.unusedExports) {
10380
+ console.log(source_default.blue(` • Exported Items: ${result.unusedExports.total}`));
10381
+ }
10382
+ console.log("");
10383
+ }
10384
+ function filterResults(result, filterPattern) {
10385
+ const filter2 = filterPattern.toLowerCase();
10386
+ console.log(source_default.blue(`\uD83D\uDD0D Filtering results by "${filter2}"...
10369
10387
  `));
10388
+ const getAppName = (filePath) => {
10389
+ if (filePath.startsWith("apps/"))
10390
+ return filePath.split("/").slice(0, 2).join("/");
10391
+ if (filePath.startsWith("packages/"))
10392
+ return filePath.split("/").slice(0, 2).join("/");
10393
+ return "Root";
10394
+ };
10395
+ const matchesFilter = (path2) => {
10396
+ const lowerPath = path2.toLowerCase();
10397
+ const appName = getAppName(path2).toLowerCase();
10398
+ if (appName.includes(filter2))
10399
+ return true;
10400
+ const segments = lowerPath.split("/");
10401
+ for (const segment of segments) {
10402
+ if (segment === filter2)
10403
+ return true;
10404
+ const withoutExt = segment.replace(/\.[^.]+$/, "");
10405
+ if (withoutExt === filter2)
10406
+ return true;
10370
10407
  }
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...
10408
+ return lowerPath.includes(filter2);
10409
+ };
10410
+ result.routes = result.routes.filter((r) => matchesFilter(r.filePath));
10411
+ if (result.publicAssets) {
10412
+ result.publicAssets.assets = result.publicAssets.assets.filter((a) => matchesFilter(a.path));
10413
+ result.publicAssets.total = result.publicAssets.assets.length;
10414
+ result.publicAssets.used = result.publicAssets.assets.filter((a) => a.used).length;
10415
+ result.publicAssets.unused = result.publicAssets.assets.filter((a) => !a.used).length;
10416
+ }
10417
+ if (result.unusedFiles) {
10418
+ result.unusedFiles.files = result.unusedFiles.files.filter((f) => matchesFilter(f.path));
10419
+ result.unusedFiles.total = result.unusedFiles.files.length;
10420
+ result.unusedFiles.unused = result.unusedFiles.files.length;
10421
+ }
10422
+ if (result.unusedExports) {
10423
+ result.unusedExports.exports = result.unusedExports.exports.filter((e) => matchesFilter(e.file));
10424
+ result.unusedExports.total = result.unusedExports.exports.length;
10425
+ result.unusedExports.unused = result.unusedExports.exports.length;
10426
+ }
10427
+ result.total = result.routes.length;
10428
+ result.used = result.routes.filter((r) => r.used).length;
10429
+ result.unused = result.routes.filter((r) => !r.used).length;
10430
+ }
10431
+ function hasUnusedItems(result) {
10432
+ const unusedRoutes = result.routes.filter((r) => !r.used).length;
10433
+ const partialRoutes = result.routes.filter((r) => r.used && r.unusedMethods.length > 0).length;
10434
+ const unusedAssets = result.publicAssets ? result.publicAssets.unused : 0;
10435
+ const unusedFiles = result.unusedFiles ? result.unusedFiles.unused : 0;
10436
+ const unusedExports = result.unusedExports ? result.unusedExports.unused : 0;
10437
+ return unusedRoutes > 0 || partialRoutes > 0 || unusedAssets > 0 || unusedFiles > 0 || unusedExports > 0;
10438
+ }
10439
+ async function handleFixes(result, config, options) {
10440
+ let fixedSomething = false;
10441
+ const unusedRoutes = result.routes.filter((r) => !r.used);
10442
+ if (unusedRoutes.length > 0) {
10443
+ console.log(source_default.yellow.bold(`\uD83D\uDDD1️ Deleting unused routes...
10375
10444
  `));
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
- }
10445
+ const routesByFile = new Map;
10446
+ for (const r of unusedRoutes) {
10447
+ const list = routesByFile.get(r.filePath) || [];
10448
+ list.push(r);
10449
+ routesByFile.set(r.filePath, list);
10450
+ }
10451
+ for (const [filePath, fileRoutes] of routesByFile) {
10452
+ const fullPath = join8(config.dir, filePath);
10453
+ if (!existsSync7(fullPath))
10454
+ continue;
10455
+ const route = fileRoutes[0];
10456
+ const routeDir = dirname4(fullPath);
10457
+ try {
10458
+ if (route.type === "nextjs") {
10459
+ if (filePath.includes("app/api") || filePath.includes("pages/api")) {
10460
+ rmSync(routeDir, { recursive: true, force: true });
10461
+ console.log(source_default.red(` Deleted Folder: ${routeDir}`));
10462
+ } else {
10463
+ rmSync(fullPath, { force: true });
10464
+ console.log(source_default.red(` Deleted File: ${filePath}`));
10465
+ }
10466
+ fixedSomething = true;
10467
+ } else if (route.type === "nestjs") {
10468
+ const isInternallyUnused = result.unusedFiles?.files.some((f) => f.path === filePath);
10469
+ if (isInternallyUnused || filePath.includes("api/")) {
10470
+ rmSync(fullPath, { force: true });
10471
+ console.log(source_default.red(` Deleted File: ${filePath}`));
10472
+ fixedSomething = true;
10473
+ } else {
10474
+ console.log(source_default.yellow(` Skipped File Deletion (internally used): ${filePath}`));
10475
+ const allMethodsToPrune = [];
10476
+ for (const r of fileRoutes) {
10477
+ for (const m of r.unusedMethods) {
10478
+ if (r.methodLines[m] !== undefined) {
10479
+ allMethodsToPrune.push({ method: m, line: r.methodLines[m] });
10420
10480
  }
10421
10481
  }
10422
- } else {
10423
- rmSync(fullPath, { force: true });
10424
- console.log(source_default.red(` Deleted File: ${filePath}`));
10425
- fixedSomething = true;
10426
10482
  }
10427
- for (const r of fileRoutes) {
10428
- const idx = result.routes.indexOf(r);
10429
- if (idx !== -1)
10430
- result.routes.splice(idx, 1);
10483
+ allMethodsToPrune.sort((a, b) => b.line - a.line);
10484
+ for (const { method, line } of allMethodsToPrune) {
10485
+ if (removeMethodFromRoute(config.dir, filePath, method, line)) {
10486
+ console.log(source_default.green(` Fixed: Removed ${method} from ${filePath}`));
10487
+ fixedSomething = true;
10488
+ }
10431
10489
  }
10432
- } catch (err) {
10433
- console.log(source_default.yellow(` Failed to fix: ${filePath}`));
10434
10490
  }
10491
+ } else {
10492
+ rmSync(fullPath, { force: true });
10493
+ console.log(source_default.red(` Deleted File: ${filePath}`));
10494
+ fixedSomething = true;
10435
10495
  }
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
- }
10496
+ for (const r of fileRoutes) {
10497
+ const idx = result.routes.indexOf(r);
10498
+ if (idx !== -1)
10499
+ result.routes.splice(idx, 1);
10460
10500
  }
10461
- console.log("");
10501
+ } catch (err) {
10502
+ console.log(source_default.yellow(` Failed to fix: ${filePath}`));
10462
10503
  }
10463
- if (result.unusedFiles && result.unusedFiles.files.length > 0) {
10464
- console.log(source_default.yellow.bold(`\uD83D\uDDD1️ Deleting unused source files...
10504
+ }
10505
+ console.log("");
10506
+ }
10507
+ const partiallyRoutes = result.routes.filter((r) => r.used && r.unusedMethods && r.unusedMethods.length > 0);
10508
+ if (partiallyRoutes.length > 0) {
10509
+ console.log(source_default.yellow.bold(`\uD83D\uDD27 Fixing partially unused routes...
10465
10510
  `));
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;
10511
+ for (const route of partiallyRoutes) {
10512
+ const sortedMethods = [...route.unusedMethods].filter((m) => route.methodLines[m] !== undefined).sort((a, b) => route.methodLines[b] - route.methodLines[a]);
10513
+ let fixedCount = 0;
10514
+ for (const method of sortedMethods) {
10515
+ const lineNum = route.methodLines[method];
10516
+ if (removeMethodFromRoute(config.dir, route.filePath, method, lineNum)) {
10517
+ console.log(source_default.green(` Fixed: Removed ${method} from ${route.path}`));
10518
+ fixedCount++;
10519
+ fixedSomething = true;
10520
+ }
10521
+ }
10522
+ if (fixedCount === route.methods.length) {
10523
+ const idx = result.routes.indexOf(route);
10524
+ if (idx !== -1)
10525
+ result.routes.splice(idx, 1);
10526
+ } else {
10527
+ route.unusedMethods = route.unusedMethods.filter((m) => !sortedMethods.includes(m));
10484
10528
  }
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.
10529
+ }
10530
+ console.log("");
10531
+ }
10532
+ if (result.unusedFiles && result.unusedFiles.files.length > 0) {
10533
+ console.log(source_default.yellow.bold(`\uD83D\uDDD1️ Deleting unused source files...
10512
10534
  `));
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;
10535
+ for (const file of result.unusedFiles.files) {
10536
+ try {
10537
+ const fullPath = join8(config.dir, file.path);
10538
+ if (!existsSync7(fullPath))
10539
+ continue;
10540
+ rmSync(fullPath, { force: true });
10541
+ console.log(source_default.red(` Deleted: ${file.path}`));
10542
+ fixedSomething = true;
10543
+ } catch (_err) {
10544
+ console.log(source_default.yellow(` Failed to delete: ${file.path}`));
10521
10545
  }
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
10546
  }
10526
- console.log(source_default.bold(`\uD83D\uDCCA Summary Report
10547
+ result.unusedFiles.files = [];
10548
+ result.unusedFiles.unused = 0;
10549
+ console.log("");
10550
+ }
10551
+ if (result.unusedExports && result.unusedExports.exports.length > 0) {
10552
+ fixedSomething = await fixUnusedExports(result, config) || fixedSomething;
10553
+ }
10554
+ if (fixedSomething) {
10555
+ console.log(source_default.cyan.bold(`
10556
+ \uD83D\uDD04 Checking for cascading dead code (newly unused implementation)...`));
10557
+ const secondPass = await scanUnusedExports(config);
10558
+ if (options.filter) {
10559
+ const filter2 = options.filter.toLowerCase();
10560
+ const getAppName = (filePath) => {
10561
+ if (filePath.startsWith("apps/"))
10562
+ return filePath.split("/").slice(0, 2).join("/");
10563
+ if (filePath.startsWith("packages/"))
10564
+ return filePath.split("/").slice(0, 2).join("/");
10565
+ return "Root";
10566
+ };
10567
+ const matchesFilter = (path2) => {
10568
+ const lowerPath = path2.toLowerCase();
10569
+ const appName = getAppName(path2).toLowerCase();
10570
+ if (appName.includes(filter2))
10571
+ return true;
10572
+ return lowerPath.includes(filter2);
10573
+ };
10574
+ secondPass.exports = secondPass.exports.filter((e) => matchesFilter(e.file));
10575
+ secondPass.total = secondPass.exports.length;
10576
+ secondPass.unused = secondPass.exports.length;
10577
+ }
10578
+ if (secondPass.unused > 0) {
10579
+ console.log(source_default.yellow(` Found ${secondPass.unused} newly unused items/methods after pruning.
10527
10580
  `));
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
- });
10581
+ result.unusedExports = secondPass;
10582
+ await fixUnusedExports(result, config);
10554
10583
  }
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
10584
  }
10568
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
10569
- console.log(source_default.dim(`
10570
- ⏱️ Completed in ${elapsed}s`));
10571
- });
10585
+ if (fixedSomething) {
10586
+ result.unused = result.routes.filter((r) => !r.used).length;
10587
+ result.used = result.routes.filter((r) => r.used).length;
10588
+ result.total = result.routes.length;
10589
+ }
10590
+ }
10572
10591
  async function fixUnusedExports(result, config) {
10573
10592
  if (!result.unusedExports || result.unusedExports.exports.length === 0)
10574
10593
  return false;
@@ -10606,4 +10625,50 @@ async function fixUnusedExports(result, config) {
10606
10625
  `));
10607
10626
  return fixedSomething;
10608
10627
  }
10609
- program2.parse();
10628
+ function printSummaryTable(result, context) {
10629
+ console.log(source_default.bold(`\uD83D\uDCCA Summary Report for ${context}
10630
+ `));
10631
+ const summary = [];
10632
+ const groupedRoutes = new Map;
10633
+ const getAppName = (filePath) => {
10634
+ if (filePath.startsWith("apps/"))
10635
+ return filePath.split("/").slice(0, 2).join("/");
10636
+ if (filePath.startsWith("packages/"))
10637
+ return filePath.split("/").slice(0, 2).join("/");
10638
+ return "Root";
10639
+ };
10640
+ for (const route of result.routes) {
10641
+ const keyAppName = getAppName(route.filePath);
10642
+ const key = `${keyAppName}::${route.type}`;
10643
+ if (!groupedRoutes.has(key)) {
10644
+ groupedRoutes.set(key, { type: route.type, app: keyAppName, routes: [] });
10645
+ }
10646
+ groupedRoutes.get(key).routes.push(route);
10647
+ }
10648
+ const sortedKeys = Array.from(groupedRoutes.keys()).sort((a, b) => {
10649
+ const [appA, typeA] = a.split("::");
10650
+ const [appB, typeB] = b.split("::");
10651
+ if (typeA !== typeB)
10652
+ return typeA === "nextjs" ? -1 : 1;
10653
+ return appA.localeCompare(appB);
10654
+ });
10655
+ for (const key of sortedKeys) {
10656
+ const group = groupedRoutes.get(key);
10657
+ const typeLabel = group.type === "nextjs" ? "Next.js" : "NestJS";
10658
+ summary.push({
10659
+ Category: `${typeLabel} (${group.app})`,
10660
+ Total: group.routes.length,
10661
+ Used: group.routes.filter((r) => r.used).length,
10662
+ Unused: group.routes.filter((r) => !r.used).length
10663
+ });
10664
+ }
10665
+ if (summary.length === 0)
10666
+ summary.push({ Category: "API Routes", Total: result.total, Used: result.used, Unused: result.unused });
10667
+ if (result.publicAssets)
10668
+ summary.push({ Category: "Public Assets", Total: result.publicAssets.total, Used: result.publicAssets.used, Unused: result.publicAssets.unused });
10669
+ if (result.unusedFiles)
10670
+ summary.push({ Category: "Source Files", Total: result.unusedFiles.total, Used: result.unusedFiles.used, Unused: result.unusedFiles.unused });
10671
+ if (result.unusedExports)
10672
+ summary.push({ Category: "Exported Items", Total: result.unusedExports.total, Used: result.unusedExports.used, Unused: result.unusedExports.unused });
10673
+ console.table(summary);
10674
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.22.0",
3
+ "version": "1.24.0",
4
4
  "description": "Find and remove unused Next.js API routes & Nest.js Controllers",
5
5
  "type": "module",
6
6
  "files": [