pruny 1.43.4 → 1.43.6

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 +108 -56
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12563,13 +12563,13 @@ var source_default = chalk;
12563
12563
 
12564
12564
  // src/index.ts
12565
12565
  var import_prompts = __toESM(require_prompts3(), 1);
12566
- import { rmSync, existsSync as existsSync10, readdirSync, lstatSync, writeFileSync as writeFileSync3 } from "node:fs";
12567
- import { dirname as dirname6, join as join10, relative as relative5, resolve as resolve4 } from "node:path";
12566
+ import { rmSync, existsSync as existsSync11, readdirSync, lstatSync, writeFileSync as writeFileSync3 } from "node:fs";
12567
+ import { dirname as dirname6, join as join11, relative as relative5, resolve as resolve4 } from "node:path";
12568
12568
 
12569
12569
  // src/scanner.ts
12570
12570
  var import_fast_glob10 = __toESM(require_out4(), 1);
12571
- import { existsSync as existsSync7, readFileSync as readFileSync10 } from "node:fs";
12572
- import { join as join7 } from "node:path";
12571
+ import { existsSync as existsSync8, readFileSync as readFileSync10 } from "node:fs";
12572
+ import { join as join8 } from "node:path";
12573
12573
 
12574
12574
  // src/patterns.ts
12575
12575
  var EXPORTED_METHOD_PATTERN = /export\s+(?:async\s+)?(?:function|const)\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)/g;
@@ -15873,7 +15873,8 @@ async function scanUnusedServices(config) {
15873
15873
 
15874
15874
  // src/scanners/broken-links.ts
15875
15875
  var import_fast_glob9 = __toESM(require_out4(), 1);
15876
- import { readFileSync as readFileSync9 } from "node:fs";
15876
+ import { readFileSync as readFileSync9, existsSync as existsSync7 } from "node:fs";
15877
+ import { join as join7 } from "node:path";
15877
15878
  var LINK_PATTERNS = [
15878
15879
  /<Link\s+[^>]*href\s*=\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15879
15880
  /router\.(push|replace)\s*\(\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
@@ -16010,13 +16011,13 @@ async function scanBrokenLinks(config) {
16010
16011
  });
16011
16012
  if (config.appSpecificScan) {
16012
16013
  const { readdirSync, lstatSync } = await import("node:fs");
16013
- const { join: join7 } = await import("node:path");
16014
- const appsDir = join7(config.appSpecificScan.rootDir, "apps");
16014
+ const { join: join8 } = await import("node:path");
16015
+ const appsDir = join8(config.appSpecificScan.rootDir, "apps");
16015
16016
  try {
16016
- const apps = readdirSync(appsDir).filter((a) => lstatSync(join7(appsDir, a)).isDirectory());
16017
+ const apps = readdirSync(appsDir).filter((a) => lstatSync(join8(appsDir, a)).isDirectory());
16017
16018
  const expoAppDirs = [];
16018
16019
  for (const app of apps) {
16019
- const appPath = join7(appsDir, app);
16020
+ const appPath = join8(appsDir, app);
16020
16021
  const frameworks = detectAppFramework(appPath);
16021
16022
  if (frameworks.includes("expo") || frameworks.includes("react-native")) {
16022
16023
  expoAppDirs.push(appPath);
@@ -16046,6 +16047,9 @@ async function scanBrokenLinks(config) {
16046
16047
  continue;
16047
16048
  allLinkPaths.add(cleaned);
16048
16049
  if (!matchesRoute(cleaned, knownRoutes, routeSegmentsList)) {
16050
+ const publicPath = join7(appDir, "public", cleaned);
16051
+ if (existsSync7(publicPath))
16052
+ continue;
16049
16053
  const ignorePatterns = [
16050
16054
  ...config.ignore.links || [],
16051
16055
  ...config.ignore.routes
@@ -16226,12 +16230,12 @@ function normalizeNestPath(path2) {
16226
16230
  return path2.replace(/\/$/, "").replace(/\?.*$/, "").replace(/\$\{[^}]+\}/g, "*").replace(/:[^/]+/g, "*").toLowerCase();
16227
16231
  }
16228
16232
  async function detectGlobalPrefix(appDir) {
16229
- const mainTsPath = join7(appDir, "src/main.ts");
16230
- const mainTsAltPath = join7(appDir, "main.ts");
16233
+ const mainTsPath = join8(appDir, "src/main.ts");
16234
+ const mainTsAltPath = join8(appDir, "main.ts");
16231
16235
  let content;
16232
- if (existsSync7(mainTsPath)) {
16236
+ if (existsSync8(mainTsPath)) {
16233
16237
  content = readFileSync10(mainTsPath, "utf-8");
16234
- } else if (existsSync7(mainTsAltPath)) {
16238
+ } else if (existsSync8(mainTsAltPath)) {
16235
16239
  content = readFileSync10(mainTsAltPath, "utf-8");
16236
16240
  } else {
16237
16241
  return "";
@@ -16290,8 +16294,8 @@ function checkRouteUsage(route, references, nestGlobalPrefix = "") {
16290
16294
  return { used, usedMethods };
16291
16295
  }
16292
16296
  function getVercelExternalPaths(dir) {
16293
- const vercelPath = join7(dir, "vercel.json");
16294
- if (!existsSync7(vercelPath)) {
16297
+ const vercelPath = join8(dir, "vercel.json");
16298
+ if (!existsSync8(vercelPath)) {
16295
16299
  return [];
16296
16300
  }
16297
16301
  try {
@@ -16322,15 +16326,15 @@ function getVercelExternalPaths(dir) {
16322
16326
  }
16323
16327
  }
16324
16328
  function getGitHubWorkflowPaths(dir) {
16325
- const workflowDir = join7(dir, ".github", "workflows");
16326
- if (!existsSync7(workflowDir))
16329
+ const workflowDir = join8(dir, ".github", "workflows");
16330
+ if (!existsSync8(workflowDir))
16327
16331
  return [];
16328
16332
  const paths = [];
16329
16333
  const apiPathPattern = /\/api\/[a-zA-Z0-9_\-/[\]]+/g;
16330
16334
  try {
16331
16335
  const files = import_fast_glob10.default.sync("*.{yml,yaml}", { cwd: workflowDir });
16332
16336
  for (const file of files) {
16333
- const content = readFileSync10(join7(workflowDir, file), "utf-8");
16337
+ const content = readFileSync10(join8(workflowDir, file), "utf-8");
16334
16338
  let match2;
16335
16339
  apiPathPattern.lastIndex = 0;
16336
16340
  while ((match2 = apiPathPattern.exec(content)) !== null) {
@@ -16349,10 +16353,10 @@ function getGitHubWorkflowPaths(dir) {
16349
16353
  function getAutoDetectedExternalRoutes(dir) {
16350
16354
  const routes = [];
16351
16355
  const packagePaths = [
16352
- join7(dir, "package.json")
16356
+ join8(dir, "package.json")
16353
16357
  ];
16354
16358
  for (const pkgPath of packagePaths) {
16355
- if (!existsSync7(pkgPath))
16359
+ if (!existsSync8(pkgPath))
16356
16360
  continue;
16357
16361
  try {
16358
16362
  const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
@@ -16398,7 +16402,7 @@ async function scan(config) {
16398
16402
  ignore: config.ignore.folders
16399
16403
  });
16400
16404
  const nextRoutes = nextFiles.map((file) => {
16401
- const fullPath = join7(scanCwd, file);
16405
+ const fullPath = join8(scanCwd, file);
16402
16406
  const content = readFileSync10(fullPath, "utf-8");
16403
16407
  const { methods, methodLines } = extractExportedMethods(content);
16404
16408
  return {
@@ -16418,7 +16422,7 @@ async function scan(config) {
16418
16422
  ignore: config.ignore.folders
16419
16423
  });
16420
16424
  const nestRoutes = nestFiles.flatMap((file) => {
16421
- const fullPath = join7(scanCwd, file);
16425
+ const fullPath = join8(scanCwd, file);
16422
16426
  const content = readFileSync10(fullPath, "utf-8");
16423
16427
  const relativePathFromRoot = fullPath.replace(config.appSpecificScan ? config.appSpecificScan.rootDir + "/" : cwd + "/", "");
16424
16428
  return extractNestRoutes(relativePathFromRoot, content, detectedGlobalPrefix);
@@ -16475,7 +16479,7 @@ async function scan(config) {
16475
16479
  const allReferences = [];
16476
16480
  const fileReferences = new Map;
16477
16481
  for (const file of sourceFiles) {
16478
- const filePath = join7(referenceScanCwd, file);
16482
+ const filePath = join8(referenceScanCwd, file);
16479
16483
  try {
16480
16484
  const content = readFileSync10(filePath, "utf-8");
16481
16485
  const refs = extractApiReferences(content);
@@ -16485,6 +16489,20 @@ async function scan(config) {
16485
16489
  }
16486
16490
  } catch {}
16487
16491
  }
16492
+ const existingNextApiPaths = new Set;
16493
+ for (const nr of nextRoutes) {
16494
+ existingNextApiPaths.add(normalizeNextPath(nr.path));
16495
+ }
16496
+ if (config.appSpecificScan) {
16497
+ const monorepoNextFiles = await import_fast_glob10.default(nextPatterns, {
16498
+ cwd: config.appSpecificScan.rootDir,
16499
+ ignore: config.ignore.folders
16500
+ });
16501
+ for (const f of monorepoNextFiles) {
16502
+ const apiPath = extractRoutePath(f);
16503
+ existingNextApiPaths.add(normalizeNextPath(apiPath));
16504
+ }
16505
+ }
16488
16506
  for (const route of routes) {
16489
16507
  if (shouldIgnore(route.path, config.ignore.routes) || shouldIgnore(route.filePath, config.ignore.routes)) {
16490
16508
  route.used = true;
@@ -16512,6 +16530,40 @@ async function scan(config) {
16512
16530
  }
16513
16531
  }
16514
16532
  }
16533
+ for (const route of routes) {
16534
+ if (route.type !== "nestjs" || !route.used)
16535
+ continue;
16536
+ const apiVariation = route.path.startsWith("/api/") ? normalizeNestPath(route.path) : normalizeNestPath("/api" + route.path);
16537
+ const hasNextReplacement = existingNextApiPaths.has(apiVariation) || [...existingNextApiPaths].some((np) => np.startsWith(apiVariation + "/") || apiVariation.startsWith(np + "/"));
16538
+ if (hasNextReplacement) {
16539
+ const nestPathDirect = normalizeNestPath(route.path);
16540
+ let hasDirectReference = false;
16541
+ for (const ref of allReferences) {
16542
+ if (ref.source !== "http-client")
16543
+ continue;
16544
+ let normalizedRef = ref.path.replace(/\s+/g, "").replace(/\$\{[^}]+\}/g, "*").replace(/\/$/, "").replace(/\?.*$/, "").replace(/\/+/g, "/").toLowerCase();
16545
+ if (normalizedRef.startsWith("*")) {
16546
+ const firstSlash = normalizedRef.indexOf("/");
16547
+ if (firstSlash !== -1)
16548
+ normalizedRef = normalizedRef.substring(firstSlash);
16549
+ }
16550
+ if (!normalizedRef.startsWith("/api/") && !normalizedRef.startsWith("/api?")) {
16551
+ if (normalizedRef === nestPathDirect || normalizedRef.startsWith(nestPathDirect + "/") || minimatch(normalizedRef, nestPathDirect)) {
16552
+ hasDirectReference = true;
16553
+ break;
16554
+ }
16555
+ }
16556
+ }
16557
+ if (!hasDirectReference) {
16558
+ if (process.env.DEBUG_PRUNY) {
16559
+ console.log(`[DEBUG] NestJS route ${route.path} de-marked: Next.js replacement exists at ${apiVariation}`);
16560
+ }
16561
+ route.used = false;
16562
+ route.references = [];
16563
+ route.unusedMethods = [...route.methods];
16564
+ }
16565
+ }
16566
+ }
16515
16567
  let publicAssets;
16516
16568
  if (!config.excludePublic) {
16517
16569
  publicAssets = await scanPublicAssets(config);
@@ -16537,8 +16589,8 @@ async function scan(config) {
16537
16589
 
16538
16590
  // src/config.ts
16539
16591
  var import_fast_glob11 = __toESM(require_out4(), 1);
16540
- import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
16541
- import { join as join8, resolve as resolve3, relative as relative4, dirname as dirname5 } from "node:path";
16592
+ import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
16593
+ import { join as join9, resolve as resolve3, relative as relative4, dirname as dirname5 } from "node:path";
16542
16594
  var DEFAULT_CONFIG = {
16543
16595
  dir: "./",
16544
16596
  ignore: {
@@ -16575,7 +16627,7 @@ function loadConfig(options) {
16575
16627
  ignore: DEFAULT_CONFIG.ignore.folders,
16576
16628
  absolute: true
16577
16629
  });
16578
- if (options.config && existsSync8(options.config)) {
16630
+ if (options.config && existsSync9(options.config)) {
16579
16631
  const absConfig = resolve3(cwd, options.config);
16580
16632
  if (!configFiles.includes(absConfig)) {
16581
16633
  configFiles.push(absConfig);
@@ -16604,7 +16656,7 @@ function loadConfig(options) {
16604
16656
  const prefixPattern = (p) => {
16605
16657
  if (p.startsWith("**/") || p.startsWith("/") || !relDir)
16606
16658
  return p;
16607
- const prefixed = join8(relDir, p).replace(/\\/g, "/");
16659
+ const prefixed = join9(relDir, p).replace(/\\/g, "/");
16608
16660
  return prefixed.includes("/") ? `**/${prefixed}` : `**/${prefixed}`;
16609
16661
  };
16610
16662
  if (config.ignore?.routes)
@@ -16643,8 +16695,8 @@ function loadConfig(options) {
16643
16695
  };
16644
16696
  }
16645
16697
  function parseGitIgnore(dir) {
16646
- const gitIgnorePath = join8(dir, ".gitignore");
16647
- if (!existsSync8(gitIgnorePath))
16698
+ const gitIgnorePath = join9(dir, ".gitignore");
16699
+ if (!existsSync9(gitIgnorePath))
16648
16700
  return [];
16649
16701
  try {
16650
16702
  const content = readFileSync11(gitIgnorePath, "utf-8");
@@ -16661,8 +16713,8 @@ function parseGitIgnore(dir) {
16661
16713
  function findConfigFile(dir) {
16662
16714
  const candidates = ["pruny.config.json", ".prunyrc.json", ".prunyrc"];
16663
16715
  for (const name of candidates) {
16664
- const path2 = join8(dir, name);
16665
- if (existsSync8(path2)) {
16716
+ const path2 = join9(dir, name);
16717
+ if (existsSync9(path2)) {
16666
16718
  return path2;
16667
16719
  }
16668
16720
  }
@@ -16670,11 +16722,11 @@ function findConfigFile(dir) {
16670
16722
  }
16671
16723
 
16672
16724
  // src/init.ts
16673
- import { writeFileSync as writeFileSync2, existsSync as existsSync9 } from "node:fs";
16674
- import { join as join9 } from "node:path";
16725
+ import { writeFileSync as writeFileSync2, existsSync as existsSync10 } from "node:fs";
16726
+ import { join as join10 } from "node:path";
16675
16727
  function init(cwd = process.cwd()) {
16676
- const configPath = join9(cwd, "pruny.config.json");
16677
- if (existsSync9(configPath)) {
16728
+ const configPath = join10(cwd, "pruny.config.json");
16729
+ if (existsSync10(configPath)) {
16678
16730
  console.log(source_default.yellow("⚠️ pruny.config.json already exists. Skipping."));
16679
16731
  return;
16680
16732
  }
@@ -16702,7 +16754,7 @@ program2.action(async (options) => {
16702
16754
  config: options.config,
16703
16755
  excludePublic: !options.public
16704
16756
  });
16705
- const absoluteDir = baseConfig.dir.startsWith("/") ? baseConfig.dir : join10(process.cwd(), baseConfig.dir);
16757
+ const absoluteDir = baseConfig.dir.startsWith("/") ? baseConfig.dir : join11(process.cwd(), baseConfig.dir);
16706
16758
  baseConfig.dir = absoluteDir;
16707
16759
  if (options.verbose)
16708
16760
  console.log("");
@@ -16710,13 +16762,13 @@ program2.action(async (options) => {
16710
16762
  \uD83D\uDD0D Scanning for unused API routes...
16711
16763
  `));
16712
16764
  let monorepoRoot = absoluteDir;
16713
- let appsDir = join10(monorepoRoot, "apps");
16714
- let isMonorepo = existsSync10(appsDir) && lstatSync(appsDir).isDirectory();
16765
+ let appsDir = join11(monorepoRoot, "apps");
16766
+ let isMonorepo = existsSync11(appsDir) && lstatSync(appsDir).isDirectory();
16715
16767
  if (!isMonorepo) {
16716
16768
  let current = absoluteDir;
16717
16769
  while (current !== dirname6(current)) {
16718
- const potentialApps = join10(current, "apps");
16719
- if (existsSync10(potentialApps) && lstatSync(potentialApps).isDirectory()) {
16770
+ const potentialApps = join11(current, "apps");
16771
+ if (existsSync11(potentialApps) && lstatSync(potentialApps).isDirectory()) {
16720
16772
  monorepoRoot = current;
16721
16773
  appsDir = potentialApps;
16722
16774
  isMonorepo = true;
@@ -16734,13 +16786,13 @@ program2.action(async (options) => {
16734
16786
  const ignoredApps = options.ignoreApps ? options.ignoreApps.split(",").map((a) => a.trim()) : [];
16735
16787
  if (isMonorepo) {
16736
16788
  if (monorepoRoot !== absoluteDir) {
16737
- const appName = relative5(join10(monorepoRoot, "apps"), absoluteDir);
16789
+ const appName = relative5(join11(monorepoRoot, "apps"), absoluteDir);
16738
16790
  appsToScan.push(appName);
16739
16791
  } else {
16740
16792
  const apps = readdirSync(appsDir);
16741
16793
  const availableApps = [];
16742
16794
  for (const app of apps) {
16743
- const appPath = join10(appsDir, app);
16795
+ const appPath = join11(appsDir, app);
16744
16796
  if (lstatSync(appPath).isDirectory()) {
16745
16797
  availableApps.push(app);
16746
16798
  }
@@ -16802,7 +16854,7 @@ program2.action(async (options) => {
16802
16854
  let appDir = monorepoRoot;
16803
16855
  if (isMonorepo) {
16804
16856
  appLabel = `App: ${appName}`;
16805
- appDir = join10(monorepoRoot, "apps", appName);
16857
+ appDir = join11(monorepoRoot, "apps", appName);
16806
16858
  currentConfig.appSpecificScan = {
16807
16859
  appDir,
16808
16860
  rootDir: monorepoRoot
@@ -17263,7 +17315,7 @@ Analyzing cascading impact...`));
17263
17315
  }));
17264
17316
  dryRunReport.uniqueFiles = brokenList.length;
17265
17317
  }
17266
- const reportPath = join10(process.cwd(), "pruny-dry-run.json");
17318
+ const reportPath = join11(process.cwd(), "pruny-dry-run.json");
17267
17319
  writeFileSync3(reportPath, JSON.stringify(dryRunReport, null, 2));
17268
17320
  console.log(source_default.green(`
17269
17321
  ✅ Dry run report saved to: ${source_default.bold(reportPath)}`));
@@ -17317,7 +17369,7 @@ Analyzing cascading impact...`));
17317
17369
  if (!asset.used) {
17318
17370
  try {
17319
17371
  const fullPath = asset.path;
17320
- if (existsSync10(fullPath)) {
17372
+ if (existsSync11(fullPath)) {
17321
17373
  rmSync(fullPath, { force: true });
17322
17374
  console.log(source_default.red(` Deleted: ${asset.relativePath}`));
17323
17375
  fixedSomething = true;
@@ -17405,7 +17457,7 @@ Analyzing cascading impact...`));
17405
17457
  }
17406
17458
  const actuallyDeleted = new Set;
17407
17459
  for (const path2 of filesToUnlink) {
17408
- if (existsSync10(path2)) {
17460
+ if (existsSync11(path2)) {
17409
17461
  rmSync(path2, { recursive: true, force: true });
17410
17462
  actuallyDeleted.add(path2);
17411
17463
  console.log(source_default.red(` Deleted: ${relative5(config.dir, path2)}`));
@@ -17418,7 +17470,7 @@ Analyzing cascading impact...`));
17418
17470
  for (const [absPath, removals] of removalsByFile) {
17419
17471
  if (actuallyDeleted.has(absPath) || actuallyDeleted.has(dirname6(absPath)))
17420
17472
  continue;
17421
- if (!existsSync10(absPath))
17473
+ if (!existsSync11(absPath))
17422
17474
  continue;
17423
17475
  const sortedRemovals = removals.sort((a, b) => b.line - a.line);
17424
17476
  for (const rem of sortedRemovals) {
@@ -17448,8 +17500,8 @@ Analyzing cascading impact...`));
17448
17500
  \uD83D\uDDD1️ Deleting unused source files...`));
17449
17501
  for (const file of result.unusedFiles.files) {
17450
17502
  try {
17451
- const fullPath = join10(config.dir, file.path);
17452
- if (!existsSync10(fullPath))
17503
+ const fullPath = join11(config.dir, file.path);
17504
+ if (!existsSync11(fullPath))
17453
17505
  continue;
17454
17506
  rmSync(fullPath, { force: true });
17455
17507
  console.log(source_default.red(` Deleted: ${file.path}`));
@@ -17486,8 +17538,8 @@ Analyzing cascading impact...`));
17486
17538
  }
17487
17539
  let fixedCount = 0;
17488
17540
  for (const [file, methods] of methodsByFile.entries()) {
17489
- const fullPath = join10(config.dir, file);
17490
- if (!existsSync10(fullPath))
17541
+ const fullPath = join11(config.dir, file);
17542
+ if (!existsSync11(fullPath))
17491
17543
  continue;
17492
17544
  const sortedMethods = methods.sort((a, b) => b.line - a.line);
17493
17545
  for (const method of sortedMethods) {
@@ -17560,8 +17612,8 @@ Analyzing cascading impact...`));
17560
17612
  }
17561
17613
  let svcFixedCount = 0;
17562
17614
  for (const [file, methods] of svcByFile.entries()) {
17563
- const fullPath = join10(config.dir, file);
17564
- if (!existsSync10(fullPath))
17615
+ const fullPath = join11(config.dir, file);
17616
+ if (!existsSync11(fullPath))
17565
17617
  continue;
17566
17618
  const sortedMethods = methods.sort((a, b) => b.line - a.line);
17567
17619
  for (const method of sortedMethods) {
@@ -17606,8 +17658,8 @@ async function fixUnusedExports(result, config) {
17606
17658
  for (const [_file, exports] of exportsByFile.entries()) {
17607
17659
  const sortedExports = exports.sort((a, b) => b.line - a.line);
17608
17660
  for (const exp of sortedExports) {
17609
- const fullPath = join10(config.dir, exp.file);
17610
- if (!existsSync10(fullPath))
17661
+ const fullPath = join11(config.dir, exp.file);
17662
+ if (!existsSync11(fullPath))
17611
17663
  continue;
17612
17664
  if (removeExportFromLine(config.dir, exp)) {
17613
17665
  console.log(source_default.green(` Fixed: ${exp.name} in ${exp.file}`));
@@ -17904,7 +17956,7 @@ function printTable(summary) {
17904
17956
  function findGitRoot(startDir) {
17905
17957
  let currentDir = startDir;
17906
17958
  while (currentDir !== dirname6(currentDir)) {
17907
- if (existsSync10(join10(currentDir, ".git"))) {
17959
+ if (existsSync11(join11(currentDir, ".git"))) {
17908
17960
  return true;
17909
17961
  }
17910
17962
  currentDir = dirname6(currentDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.43.4",
3
+ "version": "1.43.6",
4
4
  "description": "Find and remove unused Next.js API routes & Nest.js Controllers",
5
5
  "type": "module",
6
6
  "files": [