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.
Files changed (2) hide show
  1. package/dist/index.js +174 -38
  2. 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 join7 } from "node:path";
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 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) {
@@ -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").action(async (options) => {
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 : join7(process.cwd(), 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
- { Category: "API Routes", Total: result.total, Used: result.used, Unused: result.unused }
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(join7(config.dir, route.filePath));
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}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.2.10",
3
+ "version": "1.2.12",
4
4
  "description": "Find and remove unused Next.js API routes & Nest.js Controllers",
5
5
  "type": "module",
6
6
  "files": [