pruny 1.21.1 → 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 +347 -342
  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);
@@ -10072,15 +10085,20 @@ function findDeclarationIndex(lines, name, hintIndex) {
10072
10085
  if (/^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|ALL)$/.test(name)) {
10073
10086
  searchName = "@" + name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
10074
10087
  }
10075
- if (hintIndex < lines.length && lines[hintIndex].includes(searchName))
10088
+ if (hintIndex >= 0 && hintIndex < lines.length && lines[hintIndex] && lines[hintIndex].includes(searchName)) {
10076
10089
  return hintIndex;
10090
+ }
10077
10091
  for (let i = 1;i < 50; i++) {
10078
- if (hintIndex - i >= 0 && lines[hintIndex - i].includes(searchName))
10079
- return hintIndex - i;
10080
- if (hintIndex + i < lines.length && lines[hintIndex + i].includes(searchName))
10081
- return hintIndex + i;
10092
+ const prev = hintIndex - i;
10093
+ if (prev >= 0 && prev < lines.length && lines[prev] && lines[prev].includes(searchName)) {
10094
+ return prev;
10095
+ }
10096
+ const next = hintIndex + i;
10097
+ if (next >= 0 && next < lines.length && lines[next] && lines[next].includes(searchName)) {
10098
+ return next;
10099
+ }
10082
10100
  }
10083
- return lines.findIndex((l) => l.includes(searchName));
10101
+ return lines.findIndex((l) => l && l.includes(searchName));
10084
10102
  }
10085
10103
  function findDeclarationStart(lines, lineIndex) {
10086
10104
  let current = lineIndex;
@@ -10089,7 +10107,19 @@ function findDeclarationStart(lines, lineIndex) {
10089
10107
  if (prevLine.startsWith("@") || prevLine.startsWith("//") || prevLine.startsWith("/*")) {
10090
10108
  current--;
10091
10109
  } else if (prevLine === "") {
10092
- if (current > 1 && lines[current - 2].trim().startsWith("@")) {
10110
+ let foundDecoratorAbove = false;
10111
+ for (let k = 1;k <= 3; k++) {
10112
+ if (current - 1 - k >= 0) {
10113
+ const checkLine = lines[current - 1 - k].trim();
10114
+ if (checkLine.startsWith("@")) {
10115
+ foundDecoratorAbove = true;
10116
+ break;
10117
+ }
10118
+ if (checkLine !== "")
10119
+ break;
10120
+ }
10121
+ }
10122
+ if (foundDecoratorAbove) {
10093
10123
  current--;
10094
10124
  } else {
10095
10125
  break;
@@ -10116,59 +10146,50 @@ function deleteDeclaration(lines, startLine, name) {
10116
10146
  return 0;
10117
10147
  let endLine = startLine;
10118
10148
  let braceCount = 0;
10149
+ let foundMethodDefinition = false;
10119
10150
  let foundBodyOpening = false;
10120
- let reachedSignature = name === null;
10121
10151
  let foundClosing = false;
10152
+ const methodDefRegex = /^(?:export\s+)?(?:public|private|protected|static|async|readonly|\s)*[a-zA-Z0-9_$]+\s*[=(<]/;
10153
+ const methodDefRegexSimple = /^[a-zA-Z0-9_$]+\s*\(/;
10122
10154
  for (let i = startLine;i < lines.length; i++) {
10123
10155
  const line = lines[i];
10124
10156
  const trimmed = line.trim();
10125
- if (!reachedSignature && name && line.includes(name)) {
10126
- reachedSignature = true;
10157
+ const isCommentOrEmpty = trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed === "";
10158
+ const isDecorator = trimmed.startsWith("@");
10159
+ if (!foundMethodDefinition && !isDecorator && !isCommentOrEmpty && braceCount === 0) {
10160
+ if (methodDefRegex.test(trimmed) || methodDefRegexSimple.test(trimmed) || name && (trimmed.includes(` ${name}(`) || trimmed.startsWith(`${name}(`))) {
10161
+ foundMethodDefinition = true;
10162
+ }
10127
10163
  }
10128
10164
  const openBraces = (line.match(/{/g) || []).length;
10129
10165
  const closeBraces = (line.match(/}/g) || []).length;
10130
- braceCount += openBraces - closeBraces;
10131
- if (reachedSignature && !foundBodyOpening && openBraces > 0) {
10132
- foundBodyOpening = true;
10133
- }
10134
- if (foundBodyOpening && braceCount <= 0) {
10135
- endLine = i;
10136
- foundClosing = true;
10137
- break;
10138
- }
10139
- if (reachedSignature && !foundBodyOpening && braceCount === 0) {
10140
- if (trimmed.endsWith(";") || trimmed.includes("};")) {
10166
+ if (foundMethodDefinition) {
10167
+ braceCount += openBraces - closeBraces;
10168
+ if (!foundBodyOpening && openBraces > 0) {
10169
+ foundBodyOpening = true;
10170
+ }
10171
+ if (foundBodyOpening && braceCount <= 0) {
10141
10172
  endLine = i;
10142
10173
  foundClosing = true;
10143
10174
  break;
10144
10175
  }
10145
- if (i > startLine && (trimmed.startsWith("@") || trimmed.match(/^(?:export\s+)?(?:async\s+)?(?:function|const|class|let|var|public|private|protected)\s+/))) {
10146
- endLine = i - 1;
10176
+ if (!foundBodyOpening && trimmed.endsWith(";") && braceCount === 0) {
10177
+ endLine = i;
10147
10178
  foundClosing = true;
10148
10179
  break;
10149
10180
  }
10150
- if (i > startLine + 10) {
10151
- endLine = i - 1;
10152
- foundClosing = true;
10181
+ } else {
10182
+ if (i > startLine + 50) {
10153
10183
  break;
10154
10184
  }
10155
10185
  }
10156
10186
  }
10157
- if (!foundClosing && reachedSignature && lines.length > startLine) {
10158
- endLine = startLine;
10159
- for (let k = startLine;k < Math.min(lines.length, startLine + 10); k++) {
10160
- if (lines[k].trim().endsWith(";") || lines[k].trim().includes("};")) {
10161
- endLine = k;
10162
- foundClosing = true;
10163
- break;
10164
- }
10165
- }
10187
+ if (foundClosing) {
10188
+ const linesToDelete = endLine - startLine + 1;
10189
+ lines.splice(startLine, linesToDelete);
10190
+ return linesToDelete;
10166
10191
  }
10167
- if (!foundClosing)
10168
- endLine = startLine;
10169
- const linesToDelete = endLine - startLine + 1;
10170
- lines.splice(startLine, linesToDelete);
10171
- return linesToDelete;
10192
+ return 0;
10172
10193
  }
10173
10194
  function isFileEmpty(content) {
10174
10195
  return content.split(`
@@ -10240,327 +10261,265 @@ program2.command("init").description("Create a default pruny.config.json file").
10240
10261
  });
10241
10262
  program2.action(async (options) => {
10242
10263
  const startTime = Date.now();
10243
- const config = loadConfig({
10244
- dir: options.dir,
10245
- config: options.config,
10246
- excludePublic: !options.public
10247
- });
10248
- const absoluteDir = config.dir.startsWith("/") ? config.dir : join8(process.cwd(), config.dir);
10249
- config.dir = absoluteDir;
10250
- if (options.verbose) {
10251
- console.log("");
10252
- }
10253
- 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(`
10254
10275
  \uD83D\uDD0D Scanning for unused API routes...
10255
10276
  `));
10256
- try {
10257
10277
  let result = await scan(config);
10258
- const getAppName = (filePath) => {
10259
- if (filePath.startsWith("apps/"))
10260
- return filePath.split("/").slice(0, 2).join("/");
10261
- if (filePath.startsWith("packages/"))
10262
- return filePath.split("/").slice(0, 2).join("/");
10263
- return "Root";
10264
- };
10278
+ logScanStats(result);
10265
10279
  if (options.filter) {
10266
- const filter2 = options.filter.toLowerCase();
10267
- console.log(source_default.blue(`
10268
- \uD83D\uDD0D Filtering results by "${filter2}"...
10269
- `));
10270
- const matchesFilter = (path2) => {
10271
- const lowerPath = path2.toLowerCase();
10272
- const appName = getAppName(path2).toLowerCase();
10273
- if (appName.includes(filter2))
10274
- return true;
10275
- const segments = lowerPath.split("/");
10276
- for (const segment of segments) {
10277
- if (segment === filter2)
10278
- return true;
10279
- const withoutExt = segment.replace(/\.[^.]+$/, "");
10280
- if (withoutExt === filter2)
10281
- return true;
10282
- }
10283
- return lowerPath.includes(filter2);
10284
- };
10285
- result.routes = result.routes.filter((r) => matchesFilter(r.filePath));
10286
- if (result.publicAssets) {
10287
- result.publicAssets.assets = result.publicAssets.assets.filter((a) => matchesFilter(a.path));
10288
- result.publicAssets.total = result.publicAssets.assets.length;
10289
- result.publicAssets.used = result.publicAssets.assets.filter((a) => a.used).length;
10290
- result.publicAssets.unused = result.publicAssets.assets.filter((a) => !a.used).length;
10291
- }
10292
- if (result.unusedFiles) {
10293
- result.unusedFiles.files = result.unusedFiles.files.filter((f) => matchesFilter(f.path));
10294
- result.unusedFiles.total = result.unusedFiles.files.length;
10295
- result.unusedFiles.unused = result.unusedFiles.files.length;
10296
- }
10297
- if (result.unusedExports) {
10298
- result.unusedExports.exports = result.unusedExports.exports.filter((e) => matchesFilter(e.file));
10299
- result.unusedExports.total = result.unusedExports.exports.length;
10300
- result.unusedExports.unused = result.unusedExports.exports.length;
10301
- }
10280
+ filterResults(result, options.filter);
10302
10281
  }
10303
10282
  if (options.json) {
10304
10283
  console.log(JSON.stringify(result, null, 2));
10305
10284
  return;
10306
10285
  }
10307
- const partiallyUnusedRoutes = result.routes.filter((r) => r.used && r.unusedMethods.length > 0);
10308
- if (partiallyUnusedRoutes.length > 0) {
10309
- console.log(source_default.yellow.bold(`⚠️ Partially Unused API Routes:
10310
- `));
10311
- for (const route of partiallyUnusedRoutes) {
10312
- console.log(source_default.yellow(` ${route.path}`));
10313
- console.log(source_default.red(` ❌ Unused: ${route.unusedMethods.join(", ")}`));
10314
- console.log(source_default.dim(` → ${route.filePath}`));
10315
- }
10316
- console.log("");
10317
- }
10318
- const unusedRoutes = result.routes.filter((r) => !r.used);
10319
- if (unusedRoutes.length > 0) {
10320
- console.log(source_default.red.bold(`❌ Unused API Routes (Fully Unused):
10321
- `));
10322
- for (const route of unusedRoutes) {
10323
- const methods = route.methods.length > 0 ? ` (${route.methods.join(", ")})` : "";
10324
- console.log(source_default.red(` ${route.path}${source_default.dim(methods)}`));
10325
- console.log(source_default.dim(` → ${route.filePath}`));
10326
- }
10327
- console.log("");
10328
- }
10329
- if (result.publicAssets) {
10330
- const unusedAssets = result.publicAssets.assets.filter((a) => !a.used);
10331
- if (unusedAssets.length > 0) {
10332
- console.log(source_default.red.bold(`\uD83D\uDDBC️ Unused Public Assets:
10333
- `));
10334
- for (const asset of unusedAssets) {
10335
- console.log(source_default.red(` ${asset.relativePath}`));
10336
- console.log(source_default.dim(` → ${asset.path}`));
10337
- }
10338
- console.log("");
10339
- }
10340
- }
10341
- if (result.unusedFiles && result.unusedFiles.files.length > 0) {
10342
- console.log(source_default.red.bold(`\uD83D\uDCC4 Unused Source Files:
10343
- `));
10344
- for (const file of result.unusedFiles.files) {
10345
- const sizeKb = (file.size / 1024).toFixed(1);
10346
- console.log(source_default.red(` ${file.path} ${source_default.dim(`(${sizeKb} KB)`)}`));
10347
- }
10348
- console.log("");
10349
- }
10350
- if (result.unusedExports && result.unusedExports.exports.length > 0) {
10351
- 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.
10352
10290
  `));
10353
- for (const exp of result.unusedExports.exports) {
10354
- console.log(source_default.red(` ${exp.name}`));
10355
- console.log(source_default.dim(` → ${exp.file}:${exp.line}`));
10356
- }
10357
- console.log("");
10358
10291
  }
10359
- if (unusedRoutes.length === 0 && partiallyUnusedRoutes.length === 0 && (!result.publicAssets || result.publicAssets.unused === 0)) {
10360
- 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}"...
10361
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;
10362
10339
  }
10363
- if (options.fix) {
10364
- let fixedSomething = false;
10365
- if (unusedRoutes.length > 0) {
10366
- 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...
10367
10376
  `));
10368
- const routesByFile = new Map;
10369
- for (const r of unusedRoutes) {
10370
- const list = routesByFile.get(r.filePath) || [];
10371
- list.push(r);
10372
- routesByFile.set(r.filePath, list);
10373
- }
10374
- for (const [filePath, fileRoutes] of routesByFile) {
10375
- const fullPath = join8(config.dir, filePath);
10376
- if (!existsSync7(fullPath))
10377
- continue;
10378
- const route = fileRoutes[0];
10379
- const routeDir = dirname4(fullPath);
10380
- try {
10381
- if (route.type === "nextjs") {
10382
- if (filePath.includes("app/api") || filePath.includes("pages/api")) {
10383
- rmSync(routeDir, { recursive: true, force: true });
10384
- console.log(source_default.red(` Deleted Folder: ${routeDir}`));
10385
- } else {
10386
- rmSync(fullPath, { force: true });
10387
- console.log(source_default.red(` Deleted File: ${filePath}`));
10388
- }
10389
- fixedSomething = true;
10390
- } else if (route.type === "nestjs") {
10391
- const isInternallyUnused = result.unusedFiles?.files.some((f) => f.path === filePath);
10392
- if (isInternallyUnused || filePath.includes("api/")) {
10393
- rmSync(fullPath, { force: true });
10394
- console.log(source_default.red(` Deleted File: ${filePath}`));
10395
- fixedSomething = true;
10396
- } else {
10397
- console.log(source_default.yellow(` Skipped File Deletion (internally used): ${filePath}`));
10398
- const allMethodsToPrune = [];
10399
- for (const r of fileRoutes) {
10400
- for (const m of r.unusedMethods) {
10401
- if (r.methodLines[m] !== undefined) {
10402
- allMethodsToPrune.push({ method: m, line: r.methodLines[m] });
10403
- }
10404
- }
10405
- }
10406
- allMethodsToPrune.sort((a, b) => b.line - a.line);
10407
- for (const { method, line } of allMethodsToPrune) {
10408
- if (removeMethodFromRoute(config.dir, filePath, method, line)) {
10409
- console.log(source_default.green(` Fixed: Removed ${method} from ${filePath}`));
10410
- fixedSomething = true;
10411
- }
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] });
10412
10412
  }
10413
10413
  }
10414
- } else {
10415
- rmSync(fullPath, { force: true });
10416
- console.log(source_default.red(` Deleted File: ${filePath}`));
10417
- fixedSomething = true;
10418
10414
  }
10419
- for (const r of fileRoutes) {
10420
- const idx = result.routes.indexOf(r);
10421
- if (idx !== -1)
10422
- 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
+ }
10423
10421
  }
10424
- } catch (err) {
10425
- console.log(source_default.yellow(` Failed to fix: ${filePath}`));
10426
10422
  }
10423
+ } else {
10424
+ rmSync(fullPath, { force: true });
10425
+ console.log(source_default.red(` Deleted File: ${filePath}`));
10426
+ fixedSomething = true;
10427
10427
  }
10428
- console.log("");
10429
- }
10430
- const partiallyRoutes = result.routes.filter((r) => r.used && r.unusedMethods && r.unusedMethods.length > 0);
10431
- if (partiallyRoutes.length > 0) {
10432
- console.log(source_default.yellow.bold(`\uD83D\uDD27 Fixing partially unused routes...
10433
- `));
10434
- for (const route of partiallyRoutes) {
10435
- const sortedMethods = [...route.unusedMethods].filter((m) => route.methodLines[m] !== undefined).sort((a, b) => route.methodLines[b] - route.methodLines[a]);
10436
- let fixedCount = 0;
10437
- for (const method of sortedMethods) {
10438
- const lineNum = route.methodLines[method];
10439
- if (removeMethodFromRoute(config.dir, route.filePath, method, lineNum)) {
10440
- console.log(source_default.green(` Fixed: Removed ${method} from ${route.path}`));
10441
- fixedCount++;
10442
- fixedSomething = true;
10443
- }
10444
- }
10445
- if (fixedCount === route.methods.length) {
10446
- const idx = result.routes.indexOf(route);
10447
- if (idx !== -1)
10448
- result.routes.splice(idx, 1);
10449
- } else {
10450
- route.unusedMethods = route.unusedMethods.filter((m) => !sortedMethods.includes(m));
10451
- }
10428
+ for (const r of fileRoutes) {
10429
+ const idx = result.routes.indexOf(r);
10430
+ if (idx !== -1)
10431
+ result.routes.splice(idx, 1);
10452
10432
  }
10453
- console.log("");
10433
+ } catch (err) {
10434
+ console.log(source_default.yellow(` Failed to fix: ${filePath}`));
10454
10435
  }
10455
- if (result.unusedFiles && result.unusedFiles.files.length > 0) {
10456
- 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...
10457
10442
  `));
10458
- for (const file of result.unusedFiles.files) {
10459
- try {
10460
- const fullPath = join8(config.dir, file.path);
10461
- if (!existsSync7(fullPath))
10462
- continue;
10463
- rmSync(fullPath, { force: true });
10464
- console.log(source_default.red(` Deleted: ${file.path}`));
10465
- fixedSomething = true;
10466
- } catch (_err) {
10467
- console.log(source_default.yellow(` Failed to delete: ${file.path}`));
10468
- }
10469
- }
10470
- result.unusedFiles.files = [];
10471
- result.unusedFiles.unused = 0;
10472
- console.log("");
10473
- }
10474
- if (result.unusedExports && result.unusedExports.exports.length > 0) {
10475
- 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));
10476
10460
  }
10477
- if (fixedSomething) {
10478
- console.log(source_default.cyan.bold(`
10479
- \uD83D\uDD04 Checking for cascading dead code (newly unused implementation)...`));
10480
- const secondPass = await scanUnusedExports(config);
10481
- if (options.filter) {
10482
- const filter2 = options.filter.toLowerCase();
10483
- const matchesFilterPass2 = (path2) => {
10484
- const lowerPath = path2.toLowerCase();
10485
- const appName = getAppName(path2).toLowerCase();
10486
- if (appName.includes(filter2))
10487
- return true;
10488
- const segments = lowerPath.split("/");
10489
- for (const segment of segments) {
10490
- if (segment === filter2)
10491
- return true;
10492
- const withoutExt = segment.replace(/\.[^.]+$/, "");
10493
- if (withoutExt === filter2)
10494
- return true;
10495
- }
10496
- return lowerPath.includes(filter2);
10497
- };
10498
- secondPass.exports = secondPass.exports.filter((e) => matchesFilterPass2(e.file));
10499
- secondPass.total = secondPass.exports.length;
10500
- secondPass.unused = secondPass.exports.length;
10501
- }
10502
- if (secondPass.unused > 0) {
10503
- 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...
10504
10466
  `));
10505
- result.unusedExports = secondPass;
10506
- await fixUnusedExports(result, config);
10507
- }
10508
- }
10509
- if (fixedSomething) {
10510
- result.unused = result.routes.filter((r) => !r.used).length;
10511
- result.used = result.routes.filter((r) => r.used).length;
10512
- 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}`));
10513
10477
  }
10514
- } else if (unusedRoutes.length > 0 || result.unusedExports && result.unusedExports.exports.length > 0 || result.unusedFiles && result.unusedFiles.files.length > 0) {
10515
- console.log(source_default.dim(`\uD83D\uDCA1 Run with --fix to automatically clean up unused routes, files, and exports.
10516
- `));
10517
10478
  }
10518
- 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.
10519
10512
  `));
10520
- const summary = [];
10521
- const groupedRoutes = new Map;
10522
- for (const route of result.routes) {
10523
- const keyAppName = getAppName(route.filePath);
10524
- const key = `${keyAppName}::${route.type}`;
10525
- if (!groupedRoutes.has(key)) {
10526
- groupedRoutes.set(key, { type: route.type, app: keyAppName, routes: [] });
10527
- }
10528
- groupedRoutes.get(key).routes.push(route);
10529
- }
10530
- const sortedKeys = Array.from(groupedRoutes.keys()).sort((a, b) => {
10531
- const [appA, typeA] = a.split("::");
10532
- const [appB, typeB] = b.split("::");
10533
- if (typeA !== typeB)
10534
- return typeA === "nextjs" ? -1 : 1;
10535
- return appA.localeCompare(appB);
10536
- });
10537
- for (const key of sortedKeys) {
10538
- const group = groupedRoutes.get(key);
10539
- const typeLabel = group.type === "nextjs" ? "Next.js" : "NestJS";
10540
- summary.push({
10541
- Category: `${typeLabel} (${group.app})`,
10542
- Total: group.routes.length,
10543
- Used: group.routes.filter((r) => r.used).length,
10544
- Unused: group.routes.filter((r) => !r.used).length
10545
- });
10513
+ result.unusedExports = secondPass;
10514
+ await fixUnusedExports(result, config);
10546
10515
  }
10547
- if (summary.length === 0)
10548
- summary.push({ Category: "API Routes", Total: result.total, Used: result.used, Unused: result.unused });
10549
- if (result.publicAssets)
10550
- summary.push({ Category: "Public Assets", Total: result.publicAssets.total, Used: result.publicAssets.used, Unused: result.publicAssets.unused });
10551
- if (result.unusedFiles)
10552
- summary.push({ Category: "Source Files", Total: result.unusedFiles.total, Used: result.unusedFiles.used, Unused: result.unusedFiles.unused });
10553
- if (result.unusedExports)
10554
- summary.push({ Category: "Exported Items", Total: result.unusedExports.total, Used: result.unusedExports.used, Unused: result.unusedExports.unused });
10555
- console.table(summary);
10556
- } catch (_err) {
10557
- console.error(source_default.red("Error scanning:"), _err);
10558
- process.exit(1);
10559
10516
  }
10560
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
10561
- console.log(source_default.dim(`
10562
- ⏱️ Completed in ${elapsed}s`));
10563
- });
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
+ }
10564
10523
  async function fixUnusedExports(result, config) {
10565
10524
  if (!result.unusedExports || result.unusedExports.exports.length === 0)
10566
10525
  return false;
@@ -10598,4 +10557,50 @@ async function fixUnusedExports(result, config) {
10598
10557
  `));
10599
10558
  return fixedSomething;
10600
10559
  }
10601
- 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.21.1",
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": [