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.
Files changed (2) hide show
  1. package/dist/index.js +185 -36
  2. 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 routePatterns = [
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
- const routeFiles = await import_fast_glob4.default(routePatterns, {
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 routes = routeFiles.length > 0 ? routeFiles.map((file) => {
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 configPath = options.config || findConfigFile(options.dir || "./");
9606
- let fileConfig = {};
9607
- if (configPath && existsSync4(configPath)) {
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
- fileConfig = JSON.parse(content);
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: options.dir || fileConfig.dir || DEFAULT_CONFIG.dir,
9615
- ignore: {
9616
- routes: [
9617
- ...DEFAULT_CONFIG.ignore.routes || [],
9618
- ...fileConfig.ignore?.routes || []
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
- const result = await scan(config);
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
- { Category: "API Routes", Total: result.total, Used: result.used, Unused: result.unused }
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.2.11",
3
+ "version": "1.2.13",
4
4
  "description": "Find and remove unused Next.js API routes & Nest.js Controllers",
5
5
  "type": "module",
6
6
  "files": [