pruny 1.2.2 → 1.2.7

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 +184 -22
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7632,12 +7632,12 @@ 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 join5 } from "node:path";
7635
+ import { dirname as dirname2, join as join7 } from "node:path";
7636
7636
 
7637
7637
  // src/scanner.ts
7638
- var import_fast_glob3 = __toESM(require_out4(), 1);
7639
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
7640
- import { join as join3 } from "node:path";
7638
+ var import_fast_glob4 = __toESM(require_out4(), 1);
7639
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
7640
+ 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;
@@ -9300,6 +9300,111 @@ function resolveImport(baseDir, impPath, extensions, rootDir) {
9300
9300
  return null;
9301
9301
  }
9302
9302
 
9303
+ // src/scanners/unused-exports.ts
9304
+ var import_fast_glob3 = __toESM(require_out4(), 1);
9305
+ import { readFileSync as readFileSync3 } from "node:fs";
9306
+ import { join as join3 } from "node:path";
9307
+ var IGNORED_EXPORT_NAMES = new Set([
9308
+ "config",
9309
+ "generateMetadata",
9310
+ "generateStaticParams",
9311
+ "dynamic",
9312
+ "revalidate",
9313
+ "fetchCache",
9314
+ "runtime",
9315
+ "preferredRegion",
9316
+ "metadata",
9317
+ "viewport",
9318
+ "GET",
9319
+ "POST",
9320
+ "PUT",
9321
+ "DELETE",
9322
+ "PATCH",
9323
+ "HEAD",
9324
+ "OPTIONS",
9325
+ "default"
9326
+ ]);
9327
+ async function scanUnusedExports(config) {
9328
+ const cwd = config.dir;
9329
+ const extensions = config.extensions;
9330
+ const extGlob = `**/*{${extensions.join(",")}}`;
9331
+ const allFiles = await import_fast_glob3.default(extGlob, {
9332
+ cwd,
9333
+ ignore: [...config.ignore.folders, ...config.ignore.files]
9334
+ });
9335
+ if (allFiles.length === 0) {
9336
+ return { total: 0, used: 0, unused: 0, exports: [] };
9337
+ }
9338
+ const exportMap = new Map;
9339
+ const totalContents = new Map;
9340
+ let allExportsCount = 0;
9341
+ const inlineExportRegex = /^export\s+(?:async\s+)?(?:const|let|var|function|type|interface|enum|class)\s+([a-zA-Z0-9_$]+)/gm;
9342
+ const blockExportRegex = /^export\s*\{([^}]+)\}/gm;
9343
+ for (const file of allFiles) {
9344
+ try {
9345
+ const content = readFileSync3(join3(cwd, file), "utf-8");
9346
+ totalContents.set(file, content);
9347
+ const lines = content.split(`
9348
+ `);
9349
+ for (let i = 0;i < lines.length; i++) {
9350
+ const line = lines[i];
9351
+ inlineExportRegex.lastIndex = 0;
9352
+ let match2;
9353
+ while ((match2 = inlineExportRegex.exec(line)) !== null) {
9354
+ if (addExport(file, match2[1], i + 1)) {
9355
+ allExportsCount++;
9356
+ }
9357
+ }
9358
+ blockExportRegex.lastIndex = 0;
9359
+ while ((match2 = blockExportRegex.exec(line)) !== null) {
9360
+ const names = match2[1].split(",").map((n) => {
9361
+ const parts = n.trim().split(/\s+as\s+/);
9362
+ return parts[parts.length - 1];
9363
+ });
9364
+ for (const name of names) {
9365
+ if (addExport(file, name, i + 1)) {
9366
+ allExportsCount++;
9367
+ }
9368
+ }
9369
+ }
9370
+ }
9371
+ } catch {}
9372
+ }
9373
+ function addExport(file, name, line) {
9374
+ if (name && !IGNORED_EXPORT_NAMES.has(name)) {
9375
+ if (!exportMap.has(file))
9376
+ exportMap.set(file, []);
9377
+ exportMap.get(file).push({ name, line, file });
9378
+ return true;
9379
+ }
9380
+ return false;
9381
+ }
9382
+ const unusedExports = [];
9383
+ for (const [file, exports] of exportMap.entries()) {
9384
+ for (const exp of exports) {
9385
+ let isUsed = false;
9386
+ for (const [otherFile, content] of totalContents.entries()) {
9387
+ if (file === otherFile)
9388
+ continue;
9389
+ const referenceRegex = new RegExp(`\\b${exp.name}\\b`);
9390
+ if (referenceRegex.test(content)) {
9391
+ isUsed = true;
9392
+ break;
9393
+ }
9394
+ }
9395
+ if (!isUsed) {
9396
+ unusedExports.push(exp);
9397
+ }
9398
+ }
9399
+ }
9400
+ return {
9401
+ total: allExportsCount,
9402
+ used: allExportsCount - unusedExports.length,
9403
+ unused: unusedExports.length,
9404
+ exports: unusedExports
9405
+ };
9406
+ }
9407
+
9303
9408
  // src/scanner.ts
9304
9409
  function extractRoutePath(filePath) {
9305
9410
  let path2 = filePath.replace(/^src\//, "");
@@ -9358,12 +9463,12 @@ function checkRouteUsage(routePath, references) {
9358
9463
  return { used, usedMethods };
9359
9464
  }
9360
9465
  function getVercelCronPaths(dir) {
9361
- const vercelPath = join3(dir, "vercel.json");
9466
+ const vercelPath = join4(dir, "vercel.json");
9362
9467
  if (!existsSync3(vercelPath)) {
9363
9468
  return [];
9364
9469
  }
9365
9470
  try {
9366
- const content = readFileSync3(vercelPath, "utf-8");
9471
+ const content = readFileSync4(vercelPath, "utf-8");
9367
9472
  const config = JSON.parse(content);
9368
9473
  if (!config.crons) {
9369
9474
  return [];
@@ -9379,12 +9484,12 @@ async function scan(config) {
9379
9484
  "app/api/**/route.{ts,tsx,js,jsx}",
9380
9485
  "src/app/api/**/route.{ts,tsx,js,jsx}"
9381
9486
  ];
9382
- const routeFiles = await import_fast_glob3.default(routePatterns, {
9487
+ const routeFiles = await import_fast_glob4.default(routePatterns, {
9383
9488
  cwd,
9384
9489
  ignore: config.ignore.folders
9385
9490
  });
9386
9491
  const routes = routeFiles.length > 0 ? routeFiles.map((file) => {
9387
- const content = readFileSync3(join3(cwd, file), "utf-8");
9492
+ const content = readFileSync4(join4(cwd, file), "utf-8");
9388
9493
  const methods = extractExportedMethods(content);
9389
9494
  return {
9390
9495
  path: extractRoutePath(file),
@@ -9405,16 +9510,16 @@ async function scan(config) {
9405
9510
  }
9406
9511
  }
9407
9512
  const extGlob = `**/*{${config.extensions.join(",")}}`;
9408
- const sourceFiles = await import_fast_glob3.default(extGlob, {
9513
+ const sourceFiles = await import_fast_glob4.default(extGlob, {
9409
9514
  cwd,
9410
9515
  ignore: [...config.ignore.folders, ...config.ignore.files]
9411
9516
  });
9412
9517
  const allReferences = [];
9413
9518
  const fileReferences = new Map;
9414
9519
  for (const file of sourceFiles) {
9415
- const filePath = join3(cwd, file);
9520
+ const filePath = join4(cwd, file);
9416
9521
  try {
9417
- const content = readFileSync3(filePath, "utf-8");
9522
+ const content = readFileSync4(filePath, "utf-8");
9418
9523
  const refs = extractApiReferences(content);
9419
9524
  if (refs.length > 0) {
9420
9525
  fileReferences.set(file, refs);
@@ -9455,13 +9560,14 @@ async function scan(config) {
9455
9560
  unused: routes.filter((r) => !r.used).length,
9456
9561
  routes,
9457
9562
  publicAssets,
9458
- unusedFiles
9563
+ unusedFiles,
9564
+ unusedExports: await scanUnusedExports(config)
9459
9565
  };
9460
9566
  }
9461
9567
 
9462
9568
  // src/config.ts
9463
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
9464
- import { join as join4 } from "node:path";
9569
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
9570
+ import { join as join5 } from "node:path";
9465
9571
  var DEFAULT_CONFIG = {
9466
9572
  dir: "./",
9467
9573
  ignore: {
@@ -9492,7 +9598,7 @@ function loadConfig(options) {
9492
9598
  let fileConfig = {};
9493
9599
  if (configPath && existsSync4(configPath)) {
9494
9600
  try {
9495
- const content = readFileSync4(configPath, "utf-8");
9601
+ const content = readFileSync5(configPath, "utf-8");
9496
9602
  fileConfig = JSON.parse(content);
9497
9603
  } catch {}
9498
9604
  }
@@ -9519,7 +9625,7 @@ function loadConfig(options) {
9519
9625
  function findConfigFile(dir) {
9520
9626
  const candidates = ["pruny.config.json", ".prunyrc.json", ".prunyrc"];
9521
9627
  for (const name of candidates) {
9522
- const path2 = join4(dir, name);
9628
+ const path2 = join5(dir, name);
9523
9629
  if (existsSync4(path2)) {
9524
9630
  return path2;
9525
9631
  }
@@ -9527,6 +9633,32 @@ function findConfigFile(dir) {
9527
9633
  return null;
9528
9634
  }
9529
9635
 
9636
+ // src/fixer.ts
9637
+ import { readFileSync as readFileSync6, writeFileSync } from "node:fs";
9638
+ import { join as join6 } from "node:path";
9639
+ function removeExportFromLine(rootDir, exp) {
9640
+ const fullPath = join6(rootDir, exp.file);
9641
+ try {
9642
+ const content = readFileSync6(fullPath, "utf-8");
9643
+ const lines = content.split(`
9644
+ `);
9645
+ const lineIndex = exp.line - 1;
9646
+ const originalLine = lines[lineIndex];
9647
+ const exportPrefixRegex = /^(export\s+(?:async\s+)?)/;
9648
+ if (exportPrefixRegex.test(originalLine.trim())) {
9649
+ const newLine = originalLine.replace(/(\s*)export\s+/, "$1");
9650
+ lines[lineIndex] = newLine;
9651
+ writeFileSync(fullPath, lines.join(`
9652
+ `), "utf-8");
9653
+ return true;
9654
+ }
9655
+ return false;
9656
+ } catch (err) {
9657
+ console.error(`Error fixing export in ${exp.file}:`, err);
9658
+ return false;
9659
+ }
9660
+ }
9661
+
9530
9662
  // src/index.ts
9531
9663
  var program2 = new Command;
9532
9664
  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) => {
@@ -9535,7 +9667,7 @@ program2.name("pruny").description("Find and remove unused Next.js API routes").
9535
9667
  config: options.config,
9536
9668
  excludePublic: !options.public
9537
9669
  });
9538
- const absoluteDir = config.dir.startsWith("/") ? config.dir : join5(process.cwd(), config.dir);
9670
+ const absoluteDir = config.dir.startsWith("/") ? config.dir : join7(process.cwd(), config.dir);
9539
9671
  config.dir = absoluteDir;
9540
9672
  if (options.verbose) {
9541
9673
  console.log(source_default.dim(`
@@ -9595,6 +9727,15 @@ Config:`));
9595
9727
  }
9596
9728
  console.log("");
9597
9729
  }
9730
+ if (result.unusedExports && result.unusedExports.exports.length > 0) {
9731
+ console.log(source_default.red.bold(`\uD83D\uDD17 Unused Named Exports:
9732
+ `));
9733
+ for (const exp of result.unusedExports.exports) {
9734
+ console.log(source_default.red(` ${exp.name}`));
9735
+ console.log(source_default.dim(` → ${exp.file}:${exp.line}`));
9736
+ }
9737
+ console.log("");
9738
+ }
9598
9739
  if (unusedRoutes.length === 0 && partiallyUnusedRoutes.length === 0 && (!result.publicAssets || result.publicAssets.unused === 0)) {
9599
9740
  console.log(source_default.green(`✅ Everything is used! Clean as a whistle.
9600
9741
  `));
@@ -9620,6 +9761,14 @@ Config:`));
9620
9761
  Unused: result.unusedFiles.unused
9621
9762
  });
9622
9763
  }
9764
+ if (result.unusedExports && result.unusedExports.exports.length > 0) {
9765
+ summary.push({
9766
+ Category: "Exported Items",
9767
+ Total: result.unusedExports.total,
9768
+ Used: result.unusedExports.used,
9769
+ Unused: result.unusedExports.unused
9770
+ });
9771
+ }
9623
9772
  console.table(summary);
9624
9773
  console.log("");
9625
9774
  if (options.verbose) {
@@ -9646,7 +9795,7 @@ Config:`));
9646
9795
  console.log(source_default.yellow.bold(`\uD83D\uDDD1️ Deleting unused routes...
9647
9796
  `));
9648
9797
  for (const route of unusedRoutes) {
9649
- const routeDir = dirname2(join5(config.dir, route.filePath));
9798
+ const routeDir = dirname2(join7(config.dir, route.filePath));
9650
9799
  try {
9651
9800
  rmSync(routeDir, { recursive: true, force: true });
9652
9801
  console.log(source_default.red(` Deleted: ${route.filePath}`));
@@ -9654,12 +9803,25 @@ Config:`));
9654
9803
  console.log(source_default.yellow(` Failed to delete: ${route.filePath}`));
9655
9804
  }
9656
9805
  }
9657
- console.log(source_default.green(`
9658
- Deleted ${unusedRoutes.length} unused route(s).
9806
+ }
9807
+ if (result.unusedExports && result.unusedExports.exports.length > 0) {
9808
+ console.log(source_default.yellow.bold(`\uD83D\uDD27 Fixing unused exports (removing "export" keyword)...
9659
9809
  `));
9810
+ let fixedCount = 0;
9811
+ for (const exp of result.unusedExports.exports) {
9812
+ if (removeExportFromLine(config.dir, exp)) {
9813
+ console.log(source_default.green(` Fixed: ${exp.name} in ${exp.file}`));
9814
+ fixedCount++;
9815
+ }
9816
+ }
9817
+ if (fixedCount > 0) {
9818
+ console.log(source_default.green(`
9819
+ ✅ Removed "export" from ${fixedCount} item(s).
9820
+ `));
9821
+ }
9660
9822
  }
9661
- } else if (unusedRoutes.length > 0) {
9662
- console.log(source_default.dim(`\uD83D\uDCA1 Run with --fix to delete unused routes.
9823
+ } else if (unusedRoutes.length > 0 || result.unusedExports && result.unusedExports.exports.length > 0) {
9824
+ console.log(source_default.dim(`\uD83D\uDCA1 Run with --fix to automatically clean up unused routes and exports.
9663
9825
  `));
9664
9826
  }
9665
9827
  } catch (_err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.2.2",
3
+ "version": "1.2.7",
4
4
  "description": "Find and remove unused Next.js API routes",
5
5
  "type": "module",
6
6
  "files": [