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.
- package/dist/index.js +347 -342
- 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
|
-
|
|
9804
|
+
activeNextPatterns.push(...config.extraRoutePatterns);
|
|
9796
9805
|
}
|
|
9797
|
-
const nextFiles = await import_fast_glob4.default(
|
|
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
|
|
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:
|
|
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
|
|
9822
|
-
|
|
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(
|
|
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
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10126
|
-
|
|
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
|
-
|
|
10131
|
-
|
|
10132
|
-
foundBodyOpening
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
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 (
|
|
10146
|
-
endLine = i
|
|
10176
|
+
if (!foundBodyOpening && trimmed.endsWith(";") && braceCount === 0) {
|
|
10177
|
+
endLine = i;
|
|
10147
10178
|
foundClosing = true;
|
|
10148
10179
|
break;
|
|
10149
10180
|
}
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
foundClosing = true;
|
|
10181
|
+
} else {
|
|
10182
|
+
if (i > startLine + 50) {
|
|
10153
10183
|
break;
|
|
10154
10184
|
}
|
|
10155
10185
|
}
|
|
10156
10186
|
}
|
|
10157
|
-
if (
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
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
|
-
|
|
10360
|
-
|
|
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
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
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
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
10402
|
-
|
|
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
|
-
|
|
10420
|
-
|
|
10421
|
-
if (
|
|
10422
|
-
|
|
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
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
|
|
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
|
-
|
|
10433
|
+
} catch (err) {
|
|
10434
|
+
console.log(source_default.yellow(` Failed to fix: ${filePath}`));
|
|
10454
10435
|
}
|
|
10455
|
-
|
|
10456
|
-
|
|
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
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
result.
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
}
|
|
10474
|
-
|
|
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
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
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
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10521
|
-
|
|
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
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
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
|
-
|
|
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
|
+
}
|