pruny 1.2.10 → 1.2.12
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 +174 -38
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7632,7 +7632,7 @@ var source_default = chalk;
|
|
|
7632
7632
|
|
|
7633
7633
|
// src/index.ts
|
|
7634
7634
|
import { rmSync } from "node:fs";
|
|
7635
|
-
import { dirname as dirname2, join as
|
|
7635
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
7636
7636
|
|
|
7637
7637
|
// src/scanner.ts
|
|
7638
7638
|
var import_fast_glob4 = __toESM(require_out4(), 1);
|
|
@@ -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) {
|
|
@@ -9667,15 +9746,37 @@ function removeExportFromLine(rootDir, exp) {
|
|
|
9667
9746
|
}
|
|
9668
9747
|
}
|
|
9669
9748
|
|
|
9749
|
+
// src/init.ts
|
|
9750
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync5 } from "node:fs";
|
|
9751
|
+
import { join as join7 } from "node:path";
|
|
9752
|
+
function init(cwd = process.cwd()) {
|
|
9753
|
+
const configPath = join7(cwd, "pruny.config.json");
|
|
9754
|
+
if (existsSync5(configPath)) {
|
|
9755
|
+
console.log(source_default.yellow("⚠️ pruny.config.json already exists. Skipping."));
|
|
9756
|
+
return;
|
|
9757
|
+
}
|
|
9758
|
+
try {
|
|
9759
|
+
const configContent = JSON.stringify(DEFAULT_CONFIG, null, 2);
|
|
9760
|
+
writeFileSync2(configPath, configContent, "utf-8");
|
|
9761
|
+
console.log(source_default.green("✅ Created pruny.config.json"));
|
|
9762
|
+
} catch (err) {
|
|
9763
|
+
console.error(source_default.red("Error creating config file:"), err);
|
|
9764
|
+
}
|
|
9765
|
+
}
|
|
9766
|
+
|
|
9670
9767
|
// src/index.ts
|
|
9671
9768
|
var program2 = new Command;
|
|
9672
|
-
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");
|
|
9770
|
+
program2.command("init").description("Create a default pruny.config.json file").action(() => {
|
|
9771
|
+
init();
|
|
9772
|
+
});
|
|
9773
|
+
program2.action(async (options) => {
|
|
9673
9774
|
const config = loadConfig({
|
|
9674
9775
|
dir: options.dir,
|
|
9675
9776
|
config: options.config,
|
|
9676
9777
|
excludePublic: !options.public
|
|
9677
9778
|
});
|
|
9678
|
-
const absoluteDir = config.dir.startsWith("/") ? config.dir :
|
|
9779
|
+
const absoluteDir = config.dir.startsWith("/") ? config.dir : join8(process.cwd(), config.dir);
|
|
9679
9780
|
config.dir = absoluteDir;
|
|
9680
9781
|
if (options.verbose) {
|
|
9681
9782
|
console.log(source_default.dim(`
|
|
@@ -9750,9 +9851,44 @@ Config:`));
|
|
|
9750
9851
|
}
|
|
9751
9852
|
console.log(source_default.bold(`\uD83D\uDCCA Summary Report
|
|
9752
9853
|
`));
|
|
9753
|
-
const summary = [
|
|
9754
|
-
|
|
9755
|
-
|
|
9854
|
+
const summary = [];
|
|
9855
|
+
const getAppName = (filePath) => {
|
|
9856
|
+
if (filePath.startsWith("apps/"))
|
|
9857
|
+
return filePath.split("/").slice(0, 2).join("/");
|
|
9858
|
+
if (filePath.startsWith("packages/"))
|
|
9859
|
+
return filePath.split("/").slice(0, 2).join("/");
|
|
9860
|
+
return "Root";
|
|
9861
|
+
};
|
|
9862
|
+
const groupedRoutes = new Map;
|
|
9863
|
+
for (const route of result.routes) {
|
|
9864
|
+
const appName = getAppName(route.filePath);
|
|
9865
|
+
const key = `${appName}::${route.type}`;
|
|
9866
|
+
if (!groupedRoutes.has(key)) {
|
|
9867
|
+
groupedRoutes.set(key, { type: route.type, app: appName, routes: [] });
|
|
9868
|
+
}
|
|
9869
|
+
groupedRoutes.get(key).routes.push(route);
|
|
9870
|
+
}
|
|
9871
|
+
const sortedKeys = Array.from(groupedRoutes.keys()).sort((a, b) => {
|
|
9872
|
+
const [appA, typeA] = a.split("::");
|
|
9873
|
+
const [appB, typeB] = b.split("::");
|
|
9874
|
+
if (typeA !== typeB)
|
|
9875
|
+
return typeA === "nextjs" ? -1 : 1;
|
|
9876
|
+
return appA.localeCompare(appB);
|
|
9877
|
+
});
|
|
9878
|
+
for (const key of sortedKeys) {
|
|
9879
|
+
const group = groupedRoutes.get(key);
|
|
9880
|
+
const typeLabel = group.type === "nextjs" ? "Next.js" : "NestJS";
|
|
9881
|
+
const label = `${typeLabel} (${group.app})`;
|
|
9882
|
+
summary.push({
|
|
9883
|
+
Category: label,
|
|
9884
|
+
Total: group.routes.length,
|
|
9885
|
+
Used: group.routes.filter((r) => r.used).length,
|
|
9886
|
+
Unused: group.routes.filter((r) => !r.used).length
|
|
9887
|
+
});
|
|
9888
|
+
}
|
|
9889
|
+
if (summary.length === 0) {
|
|
9890
|
+
summary.push({ Category: "API Routes", Total: result.total, Used: result.used, Unused: result.unused });
|
|
9891
|
+
}
|
|
9756
9892
|
if (result.publicAssets) {
|
|
9757
9893
|
summary.push({
|
|
9758
9894
|
Category: "Public Assets",
|
|
@@ -9803,7 +9939,7 @@ Config:`));
|
|
|
9803
9939
|
console.log(source_default.yellow.bold(`\uD83D\uDDD1️ Deleting unused routes...
|
|
9804
9940
|
`));
|
|
9805
9941
|
for (const route of unusedRoutes) {
|
|
9806
|
-
const routeDir = dirname2(
|
|
9942
|
+
const routeDir = dirname2(join8(config.dir, route.filePath));
|
|
9807
9943
|
try {
|
|
9808
9944
|
rmSync(routeDir, { recursive: true, force: true });
|
|
9809
9945
|
console.log(source_default.red(` Deleted: ${route.filePath}`));
|