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.
- package/dist/index.js +416 -351
- 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
|
|
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(
|
|
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(
|
|
9265
|
+
const currentDir = dirname(currentFile);
|
|
9261
9266
|
try {
|
|
9262
|
-
const content = readFileSync2(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
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
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
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
|
|
9308
|
+
const displayPath = relative(rootDir, file);
|
|
9297
9309
|
try {
|
|
9298
|
-
const stats = statSync(
|
|
9310
|
+
const stats = statSync(file);
|
|
9299
9311
|
unusedResults.push({
|
|
9300
|
-
path:
|
|
9312
|
+
path: displayPath,
|
|
9301
9313
|
size: stats.size
|
|
9302
9314
|
});
|
|
9303
9315
|
} catch {
|
|
9304
|
-
unusedResults.push({ path:
|
|
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
|
|
9444
|
-
|
|
9445
|
-
|
|
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 (
|
|
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 =
|
|
9479
|
+
const USE_WORKERS = referenceFiles.length >= 500;
|
|
9456
9480
|
const WORKER_COUNT = 2;
|
|
9457
9481
|
if (USE_WORKERS) {
|
|
9458
|
-
console.log(`\uD83D\uDCDD Scanning ${
|
|
9459
|
-
const result = await processFilesInParallel(
|
|
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 ${
|
|
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
|
|
9506
|
+
for (const file of candidateFiles) {
|
|
9471
9507
|
try {
|
|
9472
9508
|
processedFiles++;
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
const
|
|
9476
|
-
process.stdout.write(`\r Progress: ${processedFiles}/${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
9545
|
+
const existing = exportMap.get(displayPath)?.find((e) => e.name === name);
|
|
9510
9546
|
if (!existing) {
|
|
9511
|
-
if (addExport(
|
|
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
|
-
|
|
9840
|
+
activeNextPatterns.push(...config.extraRoutePatterns);
|
|
9796
9841
|
}
|
|
9797
|
-
const nextFiles = await import_fast_glob4.default(
|
|
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
|
|
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:
|
|
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
|
|
9822
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
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
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
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
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
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
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
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
|
-
|
|
10362
|
-
|
|
10363
|
-
console.log(source_default.dim(` → ${exp.file}:${exp.line}`));
|
|
10357
|
+
}
|
|
10358
|
+
printSummaryTable(result, appLabel);
|
|
10364
10359
|
}
|
|
10365
|
-
console.log("");
|
|
10366
10360
|
}
|
|
10367
|
-
|
|
10368
|
-
|
|
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
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
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
|
-
|
|
10428
|
-
|
|
10429
|
-
if (
|
|
10430
|
-
|
|
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
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
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
|
-
|
|
10501
|
+
} catch (err) {
|
|
10502
|
+
console.log(source_default.yellow(` Failed to fix: ${filePath}`));
|
|
10462
10503
|
}
|
|
10463
|
-
|
|
10464
|
-
|
|
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
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
result.
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
}
|
|
10482
|
-
|
|
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
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
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
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10529
|
-
|
|
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
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
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
|
-
|
|
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
|
+
}
|