pruny 1.2.11 → 1.2.13
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 +185 -36
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7641,6 +7641,8 @@ import { join as join4 } from "node:path";
|
|
|
7641
7641
|
|
|
7642
7642
|
// src/patterns.ts
|
|
7643
7643
|
var EXPORTED_METHOD_PATTERN = /export\s+(?:async\s+)?(?:function|const)\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)/g;
|
|
7644
|
+
var NEST_CONTROLLER_PATTERN = /@Controller\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/;
|
|
7645
|
+
var NEST_METHOD_PATTERN = /@(Get|Post|Put|Delete|Patch|Options|Head|All)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g;
|
|
7644
7646
|
var API_METHOD_PATTERNS = [
|
|
7645
7647
|
{ regex: /axios\.get\s*\(\s*['"`]\/api\/([^'"`\s)]+)['"`]/g, method: "GET" },
|
|
7646
7648
|
{ regex: /axios\.post\s*\(\s*['"`]\/api\/([^'"`\s)]+)['"`]/g, method: "POST" },
|
|
@@ -7651,7 +7653,8 @@ var API_METHOD_PATTERNS = [
|
|
|
7651
7653
|
{ regex: /fetch\s*\(\s*['"`]\/api\/([^'"`\s)]+)['"`]/g, method: undefined },
|
|
7652
7654
|
{ regex: /fetch\s*\(\s*`\/api\/([^`\s)]+)`/g, method: undefined },
|
|
7653
7655
|
{ regex: /['"`]\/api\/([^'"`\s]+)['"`]/g, method: undefined },
|
|
7654
|
-
{ regex: /['"`](?:https?:\/\/[^/]+)?\/api\/([^'"`\s]+)['"`]/g, method: undefined }
|
|
7656
|
+
{ regex: /['"`](?:https?:\/\/[^/]+)?\/api\/([^'"`\s]+)['"`]/g, method: undefined },
|
|
7657
|
+
{ regex: /`[^`]*\/api\/([^`\s]+)`/g, method: undefined }
|
|
7655
7658
|
];
|
|
7656
7659
|
function extractApiReferences(content) {
|
|
7657
7660
|
const matches = [];
|
|
@@ -9415,10 +9418,9 @@ async function scanUnusedExports(config) {
|
|
|
9415
9418
|
|
|
9416
9419
|
// src/scanner.ts
|
|
9417
9420
|
function extractRoutePath(filePath) {
|
|
9418
|
-
let path2 = filePath.replace(/^src\//, "");
|
|
9421
|
+
let path2 = filePath.replace(/^src\//, "").replace(/^apps\/[^/]+\//, "").replace(/^packages\/[^/]+\//, "");
|
|
9419
9422
|
path2 = path2.replace(/^app\//, "");
|
|
9420
9423
|
path2 = path2.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
|
|
9421
|
-
path2 = path2.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
|
|
9422
9424
|
return "/" + path2;
|
|
9423
9425
|
}
|
|
9424
9426
|
function extractExportedMethods(content) {
|
|
@@ -9431,6 +9433,38 @@ function extractExportedMethods(content) {
|
|
|
9431
9433
|
}
|
|
9432
9434
|
return methods;
|
|
9433
9435
|
}
|
|
9436
|
+
function extractNestRoutes(filePath, content, globalPrefix = "api") {
|
|
9437
|
+
const controllerMatch = content.match(NEST_CONTROLLER_PATTERN);
|
|
9438
|
+
if (!controllerMatch)
|
|
9439
|
+
return [];
|
|
9440
|
+
const controllerPath = controllerMatch[1] || "";
|
|
9441
|
+
const routes = [];
|
|
9442
|
+
NEST_METHOD_PATTERN.lastIndex = 0;
|
|
9443
|
+
let methodMatch;
|
|
9444
|
+
while ((methodMatch = NEST_METHOD_PATTERN.exec(content)) !== null) {
|
|
9445
|
+
const methodType = methodMatch[1].toUpperCase();
|
|
9446
|
+
const methodPath = methodMatch[2] || "";
|
|
9447
|
+
const fullPath = `/${globalPrefix}/${controllerPath}/${methodPath}`.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
9448
|
+
const existing = routes.find((r) => r.path === fullPath);
|
|
9449
|
+
if (existing) {
|
|
9450
|
+
if (!existing.methods.includes(methodType)) {
|
|
9451
|
+
existing.methods.push(methodType);
|
|
9452
|
+
existing.unusedMethods.push(methodType);
|
|
9453
|
+
}
|
|
9454
|
+
} else {
|
|
9455
|
+
routes.push({
|
|
9456
|
+
type: "nestjs",
|
|
9457
|
+
path: fullPath,
|
|
9458
|
+
filePath,
|
|
9459
|
+
used: false,
|
|
9460
|
+
references: [],
|
|
9461
|
+
methods: [methodType],
|
|
9462
|
+
unusedMethods: [methodType]
|
|
9463
|
+
});
|
|
9464
|
+
}
|
|
9465
|
+
}
|
|
9466
|
+
return routes;
|
|
9467
|
+
}
|
|
9434
9468
|
function shouldIgnore(path2, ignorePatterns) {
|
|
9435
9469
|
const normalizedPath = path2.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
9436
9470
|
return ignorePatterns.some((pattern) => {
|
|
@@ -9488,18 +9522,24 @@ function getVercelCronPaths(dir) {
|
|
|
9488
9522
|
}
|
|
9489
9523
|
async function scan(config) {
|
|
9490
9524
|
const cwd = config.dir;
|
|
9491
|
-
const
|
|
9525
|
+
const nextPatterns = [
|
|
9492
9526
|
"app/api/**/route.{ts,tsx,js,jsx}",
|
|
9493
|
-
"src/app/api/**/route.{ts,tsx,js,jsx}"
|
|
9527
|
+
"src/app/api/**/route.{ts,tsx,js,jsx}",
|
|
9528
|
+
"apps/**/app/api/**/route.{ts,tsx,js,jsx}",
|
|
9529
|
+
"packages/**/app/api/**/route.{ts,tsx,js,jsx}"
|
|
9494
9530
|
];
|
|
9495
|
-
|
|
9531
|
+
if (config.extraRoutePatterns) {
|
|
9532
|
+
nextPatterns.push(...config.extraRoutePatterns);
|
|
9533
|
+
}
|
|
9534
|
+
const nextFiles = await import_fast_glob4.default(nextPatterns, {
|
|
9496
9535
|
cwd,
|
|
9497
9536
|
ignore: config.ignore.folders
|
|
9498
9537
|
});
|
|
9499
|
-
const
|
|
9538
|
+
const nextRoutes = nextFiles.map((file) => {
|
|
9500
9539
|
const content = readFileSync4(join4(cwd, file), "utf-8");
|
|
9501
9540
|
const methods = extractExportedMethods(content);
|
|
9502
9541
|
return {
|
|
9542
|
+
type: "nextjs",
|
|
9503
9543
|
path: extractRoutePath(file),
|
|
9504
9544
|
filePath: file,
|
|
9505
9545
|
used: false,
|
|
@@ -9507,7 +9547,17 @@ async function scan(config) {
|
|
|
9507
9547
|
methods,
|
|
9508
9548
|
unusedMethods: [...methods]
|
|
9509
9549
|
};
|
|
9510
|
-
})
|
|
9550
|
+
});
|
|
9551
|
+
const nestPatterns = ["**/*.controller.ts"];
|
|
9552
|
+
const nestFiles = await import_fast_glob4.default(nestPatterns, {
|
|
9553
|
+
cwd,
|
|
9554
|
+
ignore: config.ignore.folders
|
|
9555
|
+
});
|
|
9556
|
+
const nestRoutes = nestFiles.flatMap((file) => {
|
|
9557
|
+
const content = readFileSync4(join4(cwd, file), "utf-8");
|
|
9558
|
+
return extractNestRoutes(file, content, config.nestGlobalPrefix);
|
|
9559
|
+
});
|
|
9560
|
+
const routes = [...nextRoutes, ...nestRoutes];
|
|
9511
9561
|
const cronPaths = getVercelCronPaths(cwd);
|
|
9512
9562
|
for (const cronPath of cronPaths) {
|
|
9513
9563
|
const route = routes.find((r) => r.path === cronPath);
|
|
@@ -9574,8 +9624,9 @@ async function scan(config) {
|
|
|
9574
9624
|
}
|
|
9575
9625
|
|
|
9576
9626
|
// src/config.ts
|
|
9627
|
+
var import_fast_glob5 = __toESM(require_out4(), 1);
|
|
9577
9628
|
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
9578
|
-
import { join as join5 } from "node:path";
|
|
9629
|
+
import { join as join5, resolve as resolve2 } from "node:path";
|
|
9579
9630
|
var DEFAULT_CONFIG = {
|
|
9580
9631
|
dir: "./",
|
|
9581
9632
|
ignore: {
|
|
@@ -9599,35 +9650,63 @@ var DEFAULT_CONFIG = {
|
|
|
9599
9650
|
"middleware.*"
|
|
9600
9651
|
]
|
|
9601
9652
|
},
|
|
9602
|
-
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
|
9653
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
9654
|
+
nestGlobalPrefix: "api",
|
|
9655
|
+
extraRoutePatterns: []
|
|
9603
9656
|
};
|
|
9604
9657
|
function loadConfig(options) {
|
|
9605
|
-
const
|
|
9606
|
-
|
|
9607
|
-
|
|
9658
|
+
const cwd = options.dir || "./";
|
|
9659
|
+
const configFiles = import_fast_glob5.default.sync(["**/pruny.config.json", "**/.prunyrc.json", "**/.prunyrc"], {
|
|
9660
|
+
cwd,
|
|
9661
|
+
ignore: DEFAULT_CONFIG.ignore.folders,
|
|
9662
|
+
absolute: true
|
|
9663
|
+
});
|
|
9664
|
+
if (options.config && existsSync4(options.config)) {
|
|
9665
|
+
const absConfig = resolve2(cwd, options.config);
|
|
9666
|
+
if (!configFiles.includes(absConfig)) {
|
|
9667
|
+
configFiles.push(absConfig);
|
|
9668
|
+
}
|
|
9669
|
+
} else if (configFiles.length === 0) {
|
|
9670
|
+
const rootConfig = findConfigFile(cwd);
|
|
9671
|
+
if (rootConfig)
|
|
9672
|
+
configFiles.push(rootConfig);
|
|
9673
|
+
}
|
|
9674
|
+
const mergedIgnore = {
|
|
9675
|
+
routes: [...DEFAULT_CONFIG.ignore.routes || []],
|
|
9676
|
+
folders: [...DEFAULT_CONFIG.ignore.folders || []],
|
|
9677
|
+
files: [...DEFAULT_CONFIG.ignore.files || []]
|
|
9678
|
+
};
|
|
9679
|
+
let mergedExtensions = [...DEFAULT_CONFIG.extensions];
|
|
9680
|
+
let nestGlobalPrefix = DEFAULT_CONFIG.nestGlobalPrefix;
|
|
9681
|
+
let extraRoutePatterns = [...DEFAULT_CONFIG.extraRoutePatterns || []];
|
|
9682
|
+
let excludePublic = options.excludePublic ?? false;
|
|
9683
|
+
for (const configPath of configFiles) {
|
|
9608
9684
|
try {
|
|
9609
9685
|
const content = readFileSync5(configPath, "utf-8");
|
|
9610
|
-
|
|
9686
|
+
const config = JSON.parse(content);
|
|
9687
|
+
if (config.ignore?.routes)
|
|
9688
|
+
mergedIgnore.routes.push(...config.ignore.routes);
|
|
9689
|
+
if (config.ignore?.folders)
|
|
9690
|
+
mergedIgnore.folders.push(...config.ignore.folders);
|
|
9691
|
+
if (config.ignore?.files)
|
|
9692
|
+
mergedIgnore.files.push(...config.ignore.files);
|
|
9693
|
+
if (config.extensions)
|
|
9694
|
+
mergedExtensions = [...new Set([...mergedExtensions, ...config.extensions])];
|
|
9695
|
+
if (config.nestGlobalPrefix)
|
|
9696
|
+
nestGlobalPrefix = config.nestGlobalPrefix;
|
|
9697
|
+
if (config.extraRoutePatterns)
|
|
9698
|
+
extraRoutePatterns.push(...config.extraRoutePatterns);
|
|
9699
|
+
if (config.excludePublic !== undefined)
|
|
9700
|
+
excludePublic = config.excludePublic;
|
|
9611
9701
|
} catch {}
|
|
9612
9702
|
}
|
|
9613
9703
|
return {
|
|
9614
|
-
dir:
|
|
9615
|
-
ignore:
|
|
9616
|
-
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
folders: [
|
|
9621
|
-
...DEFAULT_CONFIG.ignore.folders || [],
|
|
9622
|
-
...fileConfig.ignore?.folders || []
|
|
9623
|
-
],
|
|
9624
|
-
files: [
|
|
9625
|
-
...DEFAULT_CONFIG.ignore.files || [],
|
|
9626
|
-
...fileConfig.ignore?.files || []
|
|
9627
|
-
]
|
|
9628
|
-
},
|
|
9629
|
-
extensions: fileConfig.extensions || DEFAULT_CONFIG.extensions,
|
|
9630
|
-
excludePublic: options.excludePublic ?? fileConfig.excludePublic ?? false
|
|
9704
|
+
dir: cwd,
|
|
9705
|
+
ignore: mergedIgnore,
|
|
9706
|
+
extensions: mergedExtensions,
|
|
9707
|
+
excludePublic,
|
|
9708
|
+
nestGlobalPrefix,
|
|
9709
|
+
extraRoutePatterns
|
|
9631
9710
|
};
|
|
9632
9711
|
}
|
|
9633
9712
|
function findConfigFile(dir) {
|
|
@@ -9687,7 +9766,7 @@ function init(cwd = process.cwd()) {
|
|
|
9687
9766
|
|
|
9688
9767
|
// src/index.ts
|
|
9689
9768
|
var program2 = new Command;
|
|
9690
|
-
program2.name("pruny").description("Find and remove unused Next.js API routes").version("1.0.0").option("-d, --dir <path>", "Target directory to scan", "./").option("--fix", "Delete unused API routes").option("-c, --config <path>", "Path to config file").option("--json", "Output as JSON").option("--no-public", "Disable public assets scanning").option("-v, --verbose", "Show detailed info");
|
|
9769
|
+
program2.name("pruny").description("Find and remove unused Next.js API routes").version("1.0.0").option("-d, --dir <path>", "Target directory to scan", "./").option("--fix", "Delete unused API routes").option("-c, --config <path>", "Path to config file").option("--json", "Output as JSON").option("--no-public", "Disable public assets scanning").option("-v, --verbose", "Show detailed info").option("-f, --filter <pattern>", "Filter results by file path or app name");
|
|
9691
9770
|
program2.command("init").description("Create a default pruny.config.json file").action(() => {
|
|
9692
9771
|
init();
|
|
9693
9772
|
});
|
|
@@ -9709,7 +9788,42 @@ Config:`));
|
|
|
9709
9788
|
\uD83D\uDD0D Scanning for unused API routes...
|
|
9710
9789
|
`));
|
|
9711
9790
|
try {
|
|
9712
|
-
|
|
9791
|
+
let result = await scan(config);
|
|
9792
|
+
if (options.filter) {
|
|
9793
|
+
const filter2 = options.filter.toLowerCase();
|
|
9794
|
+
console.log(source_default.blue(`
|
|
9795
|
+
\uD83D\uDD0D Filtering results by "${filter2}"...
|
|
9796
|
+
`));
|
|
9797
|
+
const getAppName2 = (filePath) => {
|
|
9798
|
+
if (filePath.startsWith("apps/"))
|
|
9799
|
+
return filePath.split("/").slice(0, 2).join("/");
|
|
9800
|
+
if (filePath.startsWith("packages/"))
|
|
9801
|
+
return filePath.split("/").slice(0, 2).join("/");
|
|
9802
|
+
return "Root";
|
|
9803
|
+
};
|
|
9804
|
+
const matchesFilter = (path2) => {
|
|
9805
|
+
return path2.toLowerCase().includes(filter2) || getAppName2(path2).toLowerCase().includes(filter2);
|
|
9806
|
+
};
|
|
9807
|
+
result.routes = result.routes.filter((r) => matchesFilter(r.filePath));
|
|
9808
|
+
if (result.publicAssets) {
|
|
9809
|
+
result.publicAssets.assets = result.publicAssets.assets.filter((a) => matchesFilter(a.path));
|
|
9810
|
+
result.publicAssets.total = result.publicAssets.assets.length;
|
|
9811
|
+
result.publicAssets.used = result.publicAssets.assets.filter((a) => a.used).length;
|
|
9812
|
+
result.publicAssets.unused = result.publicAssets.assets.filter((a) => !a.used).length;
|
|
9813
|
+
}
|
|
9814
|
+
if (result.unusedFiles) {
|
|
9815
|
+
result.unusedFiles.files = result.unusedFiles.files.filter((f) => matchesFilter(f.path));
|
|
9816
|
+
result.unusedFiles.total = result.unusedFiles.files.length;
|
|
9817
|
+
result.unusedFiles.used = 0;
|
|
9818
|
+
result.unusedFiles.unused = result.unusedFiles.files.length;
|
|
9819
|
+
}
|
|
9820
|
+
if (result.unusedExports) {
|
|
9821
|
+
result.unusedExports.exports = result.unusedExports.exports.filter((e) => matchesFilter(e.file));
|
|
9822
|
+
result.unusedExports.total = result.unusedExports.exports.length;
|
|
9823
|
+
result.unusedExports.used = 0;
|
|
9824
|
+
result.unusedExports.unused = result.unusedExports.exports.length;
|
|
9825
|
+
}
|
|
9826
|
+
}
|
|
9713
9827
|
if (options.json) {
|
|
9714
9828
|
console.log(JSON.stringify(result, null, 2));
|
|
9715
9829
|
return;
|
|
@@ -9772,9 +9886,44 @@ Config:`));
|
|
|
9772
9886
|
}
|
|
9773
9887
|
console.log(source_default.bold(`\uD83D\uDCCA Summary Report
|
|
9774
9888
|
`));
|
|
9775
|
-
const summary = [
|
|
9776
|
-
|
|
9777
|
-
|
|
9889
|
+
const summary = [];
|
|
9890
|
+
const getAppName = (filePath) => {
|
|
9891
|
+
if (filePath.startsWith("apps/"))
|
|
9892
|
+
return filePath.split("/").slice(0, 2).join("/");
|
|
9893
|
+
if (filePath.startsWith("packages/"))
|
|
9894
|
+
return filePath.split("/").slice(0, 2).join("/");
|
|
9895
|
+
return "Root";
|
|
9896
|
+
};
|
|
9897
|
+
const groupedRoutes = new Map;
|
|
9898
|
+
for (const route of result.routes) {
|
|
9899
|
+
const appName = getAppName(route.filePath);
|
|
9900
|
+
const key = `${appName}::${route.type}`;
|
|
9901
|
+
if (!groupedRoutes.has(key)) {
|
|
9902
|
+
groupedRoutes.set(key, { type: route.type, app: appName, routes: [] });
|
|
9903
|
+
}
|
|
9904
|
+
groupedRoutes.get(key).routes.push(route);
|
|
9905
|
+
}
|
|
9906
|
+
const sortedKeys = Array.from(groupedRoutes.keys()).sort((a, b) => {
|
|
9907
|
+
const [appA, typeA] = a.split("::");
|
|
9908
|
+
const [appB, typeB] = b.split("::");
|
|
9909
|
+
if (typeA !== typeB)
|
|
9910
|
+
return typeA === "nextjs" ? -1 : 1;
|
|
9911
|
+
return appA.localeCompare(appB);
|
|
9912
|
+
});
|
|
9913
|
+
for (const key of sortedKeys) {
|
|
9914
|
+
const group = groupedRoutes.get(key);
|
|
9915
|
+
const typeLabel = group.type === "nextjs" ? "Next.js" : "NestJS";
|
|
9916
|
+
const label = `${typeLabel} (${group.app})`;
|
|
9917
|
+
summary.push({
|
|
9918
|
+
Category: label,
|
|
9919
|
+
Total: group.routes.length,
|
|
9920
|
+
Used: group.routes.filter((r) => r.used).length,
|
|
9921
|
+
Unused: group.routes.filter((r) => !r.used).length
|
|
9922
|
+
});
|
|
9923
|
+
}
|
|
9924
|
+
if (summary.length === 0) {
|
|
9925
|
+
summary.push({ Category: "API Routes", Total: result.total, Used: result.used, Unused: result.unused });
|
|
9926
|
+
}
|
|
9778
9927
|
if (result.publicAssets) {
|
|
9779
9928
|
summary.push({
|
|
9780
9929
|
Category: "Public Assets",
|