pruny 1.18.0 → 1.20.0

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 CHANGED
@@ -7649,13 +7649,20 @@ var API_METHOD_PATTERNS = [
7649
7649
  { regex: /axios\.put\s*\(\s*['"`](\/[^'"`\s)]+)['"`]/g, method: "PUT" },
7650
7650
  { regex: /axios\.delete\s*\(\s*['"`](\/[^'"`\s)]+)['"`]/g, method: "DELETE" },
7651
7651
  { regex: /axios\.patch\s*\(\s*['"`](\/[^'"`\s)]+)['"`]/g, method: "PATCH" },
7652
+ { regex: /axios\.get\s*\(\s*`[^`]*?(\/[^`\s)]+)`/g, method: "GET" },
7653
+ { regex: /axios\.post\s*\(\s*`[^`]*?(\/[^`\s)]+)`/g, method: "POST" },
7654
+ { regex: /axios\.put\s*\(\s*`[^`]*?(\/[^`\s)]+)`/g, method: "PUT" },
7655
+ { regex: /axios\.delete\s*\(\s*`[^`]*?(\/[^`\s)]+)`/g, method: "DELETE" },
7656
+ { regex: /axios\.patch\s*\(\s*`[^`]*?(\/[^`\s)]+)`/g, method: "PATCH" },
7652
7657
  { regex: /useSWR\s*\(\s*['"`](\/[^'"`\s)]+)['"`]/g, method: "GET" },
7658
+ { regex: /useSWR\s*\(\s*`[^`]*?(\/[^`\s)]+)`/g, method: "GET" },
7653
7659
  { regex: /fetch\s*\(\s*['"`](\/[^'"`\s)]+)['"`]/g, method: undefined },
7654
- { regex: /fetch\s*\(\s*`[^`]*(\/[^`\s)]+)`/g, method: undefined },
7660
+ { regex: /fetch\s*\(\s*`[^`]*?(\/[^`\s)]+)`/g, method: undefined },
7655
7661
  { regex: /['"`](\/api\/[^'"`\s]+)['"`]/g, method: undefined },
7656
- { regex: /['"`](?:https?:\/\/[^/]+)?(\/api\/[^'"`\s]+)['"`]/g, method: undefined },
7657
- { regex: /`[^`]*(\/[\w-]+\/[^`\s]+)`/g, method: undefined },
7658
- { regex: /['"`](\/[\w-]{2,}\/[\w-./]+)['"`]/g, method: undefined }
7662
+ { regex: /`[^`]*?(\/api\/[^`\s]+)`/g, method: undefined },
7663
+ { regex: /`[^`]*?(\/[\w-]{2,}\/[^`\s]*)`/g, method: undefined },
7664
+ { regex: /['"`](\/[\w-]{2,}\/[^'"`\s]*)['"`]/g, method: undefined },
7665
+ { regex: /['"`](\/api\/[^'"`\s]*)['"`]/g, method: undefined }
7659
7666
  ];
7660
7667
  function extractApiReferences(content) {
7661
7668
  const matches = [];
@@ -9362,6 +9369,8 @@ var IGNORED_EXPORT_NAMES = new Set([
9362
9369
  "OPTIONS",
9363
9370
  "default"
9364
9371
  ]);
9372
+ var NEST_LIFECYCLE_METHODS = new Set(["constructor", "onModuleInit", "onApplicationBootstrap", "onModuleDestroy", "beforeApplicationShutdown", "onApplicationShutdown"]);
9373
+ var classMethodRegex = /^\s*(?:async\s+)?([a-zA-Z0-9_$]+)\s*\([^)]*\)\s*(?::\s*[^\{]*)?\{/gm;
9365
9374
  async function processFilesInParallel(files, cwd, workerCount) {
9366
9375
  const __filename2 = fileURLToPath(import.meta.url);
9367
9376
  const __dirname2 = dirname2(__filename2);
@@ -9468,6 +9477,7 @@ async function scanUnusedExports(config) {
9468
9477
  }
9469
9478
  const content = readFileSync3(join3(cwd, file), "utf-8");
9470
9479
  totalContents.set(file, content);
9480
+ const isService = file.endsWith(".service.ts") || file.endsWith(".service.tsx");
9471
9481
  const lines = content.split(`
9472
9482
  `);
9473
9483
  for (let i = 0;i < lines.length; i++) {
@@ -9491,6 +9501,20 @@ async function scanUnusedExports(config) {
9491
9501
  }
9492
9502
  }
9493
9503
  }
9504
+ if (isService) {
9505
+ classMethodRegex.lastIndex = 0;
9506
+ while ((match2 = classMethodRegex.exec(line)) !== null) {
9507
+ const name = match2[1];
9508
+ if (name && !NEST_LIFECYCLE_METHODS.has(name) && !IGNORED_EXPORT_NAMES.has(name)) {
9509
+ const existing = exportMap.get(file)?.find((e) => e.name === name);
9510
+ if (!existing) {
9511
+ if (addExport(file, name, i + 1)) {
9512
+ allExportsCount++;
9513
+ }
9514
+ }
9515
+ }
9516
+ }
9517
+ }
9494
9518
  }
9495
9519
  } catch {}
9496
9520
  }
@@ -9704,21 +9728,32 @@ function normalizeNextPath(path2) {
9704
9728
  function normalizeNestPath(path2) {
9705
9729
  return path2.replace(/\/$/, "").replace(/\?.*$/, "").replace(/:[^/]+/g, "*").toLowerCase();
9706
9730
  }
9707
- function checkRouteUsage(route, references, nestGlobalPrefix = "api") {
9731
+ function checkRouteUsage(route, references, nestGlobalPrefix = "") {
9708
9732
  const normalize = route.type === "nextjs" ? normalizeNextPath : normalizeNestPath;
9709
9733
  const normalizedRoute = normalize(route.path);
9710
- const prefixToRemove = `/${nestGlobalPrefix}`;
9711
- const normalizedRouteNoPrefix = route.type === "nestjs" && route.path.startsWith(prefixToRemove) ? normalize(route.path.substring(prefixToRemove.length)) : null;
9734
+ const variations = new Set([normalizedRoute]);
9735
+ if (route.type === "nestjs") {
9736
+ if (nestGlobalPrefix) {
9737
+ const prefixToRemove = `/${nestGlobalPrefix}`.replace(/\/+/g, "/");
9738
+ if (route.path.startsWith(prefixToRemove)) {
9739
+ variations.add(normalize(route.path.substring(prefixToRemove.length)));
9740
+ }
9741
+ }
9742
+ if (route.path.startsWith("/api/")) {
9743
+ variations.add(normalize(route.path.substring(4)));
9744
+ } else {
9745
+ variations.add(normalize("/api" + route.path));
9746
+ }
9747
+ }
9712
9748
  const usedMethods = new Set;
9713
9749
  let used = false;
9714
9750
  for (const ref of references) {
9715
9751
  const normalizedFound = ref.path.replace(/\/$/, "").replace(/\?.*$/, "").replace(/\$\{[^}]+\}/g, "*").toLowerCase();
9716
9752
  let match2 = false;
9717
- if (normalizedRoute === normalizedFound || normalizedFound.startsWith(normalizedRoute + "/") || minimatch(normalizedFound, normalizedRoute)) {
9718
- match2 = true;
9719
- } else if (normalizedRouteNoPrefix) {
9720
- if (normalizedRouteNoPrefix === normalizedFound || normalizedFound.startsWith(normalizedRouteNoPrefix + "/") || minimatch(normalizedFound, normalizedRouteNoPrefix)) {
9753
+ for (const v of variations) {
9754
+ if (v === normalizedFound || normalizedFound.startsWith(v + "/") || minimatch(normalizedFound, v)) {
9721
9755
  match2 = true;
9756
+ break;
9722
9757
  }
9723
9758
  }
9724
9759
  if (match2) {
@@ -9895,7 +9930,7 @@ var DEFAULT_CONFIG = {
9895
9930
  ]
9896
9931
  },
9897
9932
  extensions: [".ts", ".tsx", ".js", ".jsx"],
9898
- nestGlobalPrefix: "api",
9933
+ nestGlobalPrefix: "",
9899
9934
  extraRoutePatterns: []
9900
9935
  };
9901
9936
  function loadConfig(options) {
@@ -10185,13 +10220,11 @@ program2.action(async (options) => {
10185
10220
  if (result.unusedFiles) {
10186
10221
  result.unusedFiles.files = result.unusedFiles.files.filter((f) => matchesFilter(f.path));
10187
10222
  result.unusedFiles.total = result.unusedFiles.files.length;
10188
- result.unusedFiles.used = 0;
10189
10223
  result.unusedFiles.unused = result.unusedFiles.files.length;
10190
10224
  }
10191
10225
  if (result.unusedExports) {
10192
10226
  result.unusedExports.exports = result.unusedExports.exports.filter((e) => matchesFilter(e.file));
10193
10227
  result.unusedExports.total = result.unusedExports.exports.length;
10194
- result.unusedExports.used = 0;
10195
10228
  result.unusedExports.unused = result.unusedExports.exports.length;
10196
10229
  }
10197
10230
  }
@@ -10243,7 +10276,7 @@ program2.action(async (options) => {
10243
10276
  console.log("");
10244
10277
  }
10245
10278
  if (result.unusedExports && result.unusedExports.exports.length > 0) {
10246
- console.log(source_default.red.bold(`\uD83D\uDD17 Unused Named Exports:
10279
+ console.log(source_default.red.bold(`\uD83D\uDD17 Unused Named Exports/Methods:
10247
10280
  `));
10248
10281
  for (const exp of result.unusedExports.exports) {
10249
10282
  console.log(source_default.red(` ${exp.name}`));
@@ -10290,7 +10323,6 @@ program2.action(async (options) => {
10290
10323
  fixedSomething = true;
10291
10324
  } else {
10292
10325
  console.log(source_default.yellow(` Skipped File Deletion (internally used): ${filePath}`));
10293
- console.log(source_default.dim(` → This controller is imported in another file (e.g. app.module.ts).`));
10294
10326
  const allMethodsToPrune = [];
10295
10327
  for (const r of fileRoutes) {
10296
10328
  for (const m of r.unusedMethods) {
@@ -10368,38 +10400,17 @@ program2.action(async (options) => {
10368
10400
  console.log("");
10369
10401
  }
10370
10402
  if (result.unusedExports && result.unusedExports.exports.length > 0) {
10371
- console.log(source_default.yellow.bold(`\uD83D\uDD27 Fixing unused exports (removing "export" keyword)...
10372
- `));
10373
- const exportsByFile = new Map;
10374
- for (const exp of result.unusedExports.exports) {
10375
- if (!exportsByFile.has(exp.file)) {
10376
- exportsByFile.set(exp.file, []);
10377
- }
10378
- exportsByFile.get(exp.file).push(exp);
10379
- }
10380
- let fixedCount = 0;
10381
- for (const [file, exports] of exportsByFile.entries()) {
10382
- const sortedExports = exports.sort((a, b) => b.line - a.line);
10383
- for (const exp of sortedExports) {
10384
- const fullPath = join8(config.dir, exp.file);
10385
- if (!existsSync7(fullPath))
10386
- continue;
10387
- if (removeExportFromLine(config.dir, exp)) {
10388
- console.log(source_default.green(` Fixed: ${exp.name} in ${exp.file}`));
10389
- fixedCount++;
10390
- fixedSomething = true;
10391
- const expIdx = result.unusedExports.exports.indexOf(exp);
10392
- if (expIdx !== -1) {
10393
- result.unusedExports.exports.splice(expIdx, 1);
10394
- result.unusedExports.unused--;
10395
- }
10396
- }
10397
- }
10398
- }
10399
- if (fixedCount > 0) {
10400
- console.log(source_default.green(`
10401
- ✅ Removed "export" from ${fixedCount} item(s).
10403
+ fixedSomething = await fixUnusedExports(result, config) || fixedSomething;
10404
+ }
10405
+ if (fixedSomething) {
10406
+ console.log(source_default.cyan.bold(`
10407
+ \uD83D\uDD04 Checking for cascading dead code (newly unused implementation)...`));
10408
+ const secondPass = await scanUnusedExports(config);
10409
+ if (secondPass.unused > 0) {
10410
+ console.log(source_default.yellow(` Found ${secondPass.unused} newly unused items/methods after pruning.
10402
10411
  `));
10412
+ result.unusedExports = secondPass;
10413
+ await fixUnusedExports(result, config);
10403
10414
  }
10404
10415
  }
10405
10416
  if (fixedSomething) {
@@ -10433,41 +10444,21 @@ program2.action(async (options) => {
10433
10444
  for (const key of sortedKeys) {
10434
10445
  const group = groupedRoutes.get(key);
10435
10446
  const typeLabel = group.type === "nextjs" ? "Next.js" : "NestJS";
10436
- const label = `${typeLabel} (${group.app})`;
10437
10447
  summary.push({
10438
- Category: label,
10448
+ Category: `${typeLabel} (${group.app})`,
10439
10449
  Total: group.routes.length,
10440
10450
  Used: group.routes.filter((r) => r.used).length,
10441
10451
  Unused: group.routes.filter((r) => !r.used).length
10442
10452
  });
10443
10453
  }
10444
- if (summary.length === 0) {
10454
+ if (summary.length === 0)
10445
10455
  summary.push({ Category: "API Routes", Total: result.total, Used: result.used, Unused: result.unused });
10446
- }
10447
- if (result.publicAssets) {
10448
- summary.push({
10449
- Category: "Public Assets",
10450
- Total: result.publicAssets.total,
10451
- Used: result.publicAssets.used,
10452
- Unused: result.publicAssets.unused
10453
- });
10454
- }
10455
- if (result.unusedFiles) {
10456
- summary.push({
10457
- Category: "Source Files",
10458
- Total: result.unusedFiles.total,
10459
- Used: result.unusedFiles.used,
10460
- Unused: result.unusedFiles.unused
10461
- });
10462
- }
10463
- if (result.unusedExports) {
10464
- summary.push({
10465
- Category: "Exported Items",
10466
- Total: result.unusedExports.total,
10467
- Used: result.unusedExports.used,
10468
- Unused: result.unusedExports.unused
10469
- });
10470
- }
10456
+ if (result.publicAssets)
10457
+ summary.push({ Category: "Public Assets", Total: result.publicAssets.total, Used: result.publicAssets.used, Unused: result.publicAssets.unused });
10458
+ if (result.unusedFiles)
10459
+ summary.push({ Category: "Source Files", Total: result.unusedFiles.total, Used: result.unusedFiles.used, Unused: result.unusedFiles.unused });
10460
+ if (result.unusedExports)
10461
+ summary.push({ Category: "Exported Items", Total: result.unusedExports.total, Used: result.unusedExports.used, Unused: result.unusedExports.unused });
10471
10462
  console.table(summary);
10472
10463
  } catch (_err) {
10473
10464
  console.error(source_default.red("Error scanning:"), _err);
@@ -10477,4 +10468,41 @@ program2.action(async (options) => {
10477
10468
  console.log(source_default.dim(`
10478
10469
  ⏱️ Completed in ${elapsed}s`));
10479
10470
  });
10471
+ async function fixUnusedExports(result, config) {
10472
+ if (!result.unusedExports || result.unusedExports.exports.length === 0)
10473
+ return false;
10474
+ console.log(source_default.yellow.bold(`\uD83D\uDD27 Fixing unused exports/methods...
10475
+ `));
10476
+ const exportsByFile = new Map;
10477
+ for (const exp of result.unusedExports.exports) {
10478
+ if (!exportsByFile.has(exp.file))
10479
+ exportsByFile.set(exp.file, []);
10480
+ exportsByFile.get(exp.file).push(exp);
10481
+ }
10482
+ let fixedCount = 0;
10483
+ let fixedSomething = false;
10484
+ for (const [file, exports] of exportsByFile.entries()) {
10485
+ const sortedExports = exports.sort((a, b) => b.line - a.line);
10486
+ for (const exp of sortedExports) {
10487
+ const fullPath = join8(config.dir, exp.file);
10488
+ if (!existsSync7(fullPath))
10489
+ continue;
10490
+ if (removeExportFromLine(config.dir, exp)) {
10491
+ console.log(source_default.green(` Fixed: ${exp.name} in ${exp.file}`));
10492
+ fixedCount++;
10493
+ fixedSomething = true;
10494
+ const expIdx = result.unusedExports.exports.indexOf(exp);
10495
+ if (expIdx !== -1) {
10496
+ result.unusedExports.exports.splice(expIdx, 1);
10497
+ result.unusedExports.unused--;
10498
+ }
10499
+ }
10500
+ }
10501
+ }
10502
+ if (fixedCount > 0)
10503
+ console.log(source_default.green(`
10504
+ ✅ Cleaned up ${fixedCount} unused item(s).
10505
+ `));
10506
+ return fixedSomething;
10507
+ }
10480
10508
  program2.parse();
@@ -22,11 +22,11 @@ var IGNORED_EXPORT_NAMES = new Set([
22
22
  "POST",
23
23
  "PUT",
24
24
  "PATCH",
25
- "DELETE",
26
- "HEAD",
27
25
  "OPTIONS",
28
26
  "default"
29
27
  ]);
28
+ var NEST_LIFECYCLE_METHODS = new Set(["constructor", "onModuleInit", "onApplicationBootstrap", "onModuleDestroy", "beforeApplicationShutdown", "onApplicationShutdown"]);
29
+ var classMethodRegex = /^\s*(?:async\s+)?([a-zA-Z0-9_$]+)\s*\([^)]*\)\s*(?::\s*[^\{]*)?\{/gm;
30
30
  var inlineExportRegex = /^export\s+(?:async\s+)?(?:const|let|var|function|type|interface|enum|class)\s+([a-zA-Z0-9_$]+)/gm;
31
31
  var blockExportRegex = /^export\s*\{([^}]+)\}/gm;
32
32
  if (parentPort && workerData) {
@@ -40,6 +40,7 @@ if (parentPort && workerData) {
40
40
  contents.set(file, content);
41
41
  const lines = content.split(`
42
42
  `);
43
+ const isService = file.endsWith(".service.ts") || file.endsWith(".service.tsx");
43
44
  for (let i = 0;i < lines.length; i++) {
44
45
  const line = lines[i];
45
46
  inlineExportRegex.lastIndex = 0;
@@ -66,6 +67,20 @@ if (parentPort && workerData) {
66
67
  }
67
68
  }
68
69
  }
70
+ if (isService) {
71
+ classMethodRegex.lastIndex = 0;
72
+ while ((match = classMethodRegex.exec(line)) !== null) {
73
+ const name = match[1];
74
+ if (name && !NEST_LIFECYCLE_METHODS.has(name) && !IGNORED_EXPORT_NAMES.has(name)) {
75
+ const existing = exportMap.get(file)?.find((e) => e.name === name);
76
+ if (!existing) {
77
+ if (!exportMap.has(file))
78
+ exportMap.set(file, []);
79
+ exportMap.get(file).push({ name, line: i + 1, file });
80
+ }
81
+ }
82
+ }
83
+ }
69
84
  }
70
85
  processedCount++;
71
86
  if (processedCount % 10 === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "description": "Find and remove unused Next.js API routes & Nest.js Controllers",
5
5
  "type": "module",
6
6
  "files": [