pruny 1.23.0 → 1.24.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.
Files changed (2) hide show
  1. package/dist/index.js +136 -68
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7631,7 +7631,7 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
7631
7631
  var source_default = chalk;
7632
7632
 
7633
7633
  // src/index.ts
7634
- import { rmSync, existsSync as existsSync7 } from "node:fs";
7634
+ import { rmSync, existsSync as existsSync7, readdirSync, lstatSync } from "node:fs";
7635
7635
  import { dirname as dirname4, join as join8 } from "node:path";
7636
7636
 
7637
7637
  // src/scanner.ts
@@ -9193,12 +9193,16 @@ var import_fast_glob2 = __toESM(require_out4(), 1);
9193
9193
  import { readFileSync as readFileSync2, statSync, existsSync as existsSync2 } from "node:fs";
9194
9194
  import { join as join2, dirname, resolve, relative, sep as sep2 } from "node:path";
9195
9195
  async function scanUnusedFiles(config) {
9196
- const cwd = config.dir;
9196
+ const rootDir = config.appSpecificScan ? config.appSpecificScan.rootDir : config.dir;
9197
+ const searchDir = config.appSpecificScan ? config.appSpecificScan.appDir : config.dir;
9197
9198
  const extensions = config.extensions;
9198
9199
  const extGlob = `**/*{${extensions.join(",")}}`;
9200
+ console.log(`
9201
+ \uD83D\uDD0D Finding source files in: ${searchDir}`);
9199
9202
  const allFiles = await import_fast_glob2.default(extGlob, {
9200
- cwd,
9201
- ignore: [...config.ignore.folders, ...config.ignore.files]
9203
+ cwd: searchDir,
9204
+ ignore: [...config.ignore.folders, ...config.ignore.files],
9205
+ absolute: true
9202
9206
  });
9203
9207
  if (allFiles.length === 0) {
9204
9208
  return { total: 0, used: 0, unused: 0, files: [] };
@@ -9245,8 +9249,9 @@ async function scanUnusedFiles(config) {
9245
9249
  "**/api/index.ts"
9246
9250
  ];
9247
9251
  for (const file of allFiles) {
9252
+ const relPath = relative(searchDir, file);
9248
9253
  const isEntry = entryPatterns.some((pattern) => {
9249
- return minimatch(file, pattern, { dot: true });
9254
+ return minimatch(relPath, pattern, { dot: true });
9250
9255
  });
9251
9256
  if (isEntry)
9252
9257
  entryFiles.add(file);
@@ -9257,9 +9262,9 @@ async function scanUnusedFiles(config) {
9257
9262
  const importRegex = /from\s+['"]([^'"]+)['"]|import\(['"]([^'"]+)['"]\)|require\(['"]([^'"]+)['"]\)/g;
9258
9263
  while (queue.length > 0) {
9259
9264
  const currentFile = queue.shift();
9260
- const currentDir = dirname(join2(cwd, currentFile));
9265
+ const currentDir = dirname(currentFile);
9261
9266
  try {
9262
- const content = readFileSync2(join2(cwd, currentFile), "utf-8");
9267
+ const content = readFileSync2(currentFile, "utf-8");
9263
9268
  let match2;
9264
9269
  importRegex.lastIndex = 0;
9265
9270
  while ((match2 = importRegex.exec(content)) !== null) {
@@ -9268,23 +9273,30 @@ async function scanUnusedFiles(config) {
9268
9273
  continue;
9269
9274
  let resolvedFile = null;
9270
9275
  if (imp.startsWith(".")) {
9271
- resolvedFile = resolveImport(currentDir, imp, extensions, cwd);
9276
+ resolvedFile = resolveImport(currentDir, imp, extensions, rootDir);
9272
9277
  } else if (imp.startsWith("@/") || imp.startsWith("~/")) {
9273
9278
  const aliasPath = imp.substring(2);
9274
- resolvedFile = resolveImport(cwd, aliasPath, extensions, cwd) || resolveImport(join2(cwd, "src"), aliasPath, extensions, cwd) || resolveImport(join2(cwd, "app"), aliasPath, extensions, cwd);
9279
+ resolvedFile = resolveImport(rootDir, aliasPath, extensions, rootDir) || resolveImport(join2(rootDir, "src"), aliasPath, extensions, rootDir) || resolveImport(join2(rootDir, "app"), aliasPath, extensions, rootDir);
9275
9280
  if (!resolvedFile) {
9276
- const pathParts = currentFile.split(/[/\\]/);
9277
- if (pathParts.length >= 2 && (pathParts[0] === "apps" || pathParts[0] === "packages")) {
9278
- const projectRoot = join2(cwd, pathParts[0], pathParts[1]);
9279
- resolvedFile = resolveImport(projectRoot, aliasPath, extensions, cwd) || resolveImport(join2(projectRoot, "src"), aliasPath, extensions, cwd) || resolveImport(join2(projectRoot, "app"), aliasPath, extensions, cwd);
9281
+ const pathParts = currentFile.split(sep2);
9282
+ const appsIndex = pathParts.lastIndexOf("apps");
9283
+ const packagesIndex = pathParts.lastIndexOf("packages");
9284
+ const index = Math.max(appsIndex, packagesIndex);
9285
+ if (index !== -1 && index + 1 < pathParts.length) {
9286
+ const projectRoot = pathParts.slice(0, index + 2).join(sep2);
9287
+ resolvedFile = resolveImport(projectRoot, aliasPath, extensions, rootDir) || resolveImport(join2(projectRoot, "src"), aliasPath, extensions, rootDir) || resolveImport(join2(projectRoot, "app"), aliasPath, extensions, rootDir);
9280
9288
  }
9281
9289
  }
9282
9290
  }
9283
- if (resolvedFile && allFilesSet.has(resolvedFile)) {
9284
- usedFiles.add(resolvedFile);
9285
- if (!visited.has(resolvedFile)) {
9286
- visited.add(resolvedFile);
9287
- queue.push(resolvedFile);
9291
+ if (resolvedFile) {
9292
+ const absoluteResolved = join2(rootDir, resolvedFile);
9293
+ const absoluteTarget = resolve(rootDir, resolvedFile);
9294
+ if (!visited.has(absoluteTarget) && existsSync2(absoluteTarget) && statSync(absoluteTarget).isFile()) {
9295
+ visited.add(absoluteTarget);
9296
+ usedFiles.add(absoluteTarget);
9297
+ queue.push(absoluteTarget);
9298
+ } else {
9299
+ usedFiles.add(absoluteTarget);
9288
9300
  }
9289
9301
  }
9290
9302
  }
@@ -9293,15 +9305,15 @@ async function scanUnusedFiles(config) {
9293
9305
  const unusedResults = [];
9294
9306
  for (const file of allFiles) {
9295
9307
  if (!usedFiles.has(file)) {
9296
- const fullPath = join2(cwd, file);
9308
+ const displayPath = relative(rootDir, file);
9297
9309
  try {
9298
- const stats = statSync(fullPath);
9310
+ const stats = statSync(file);
9299
9311
  unusedResults.push({
9300
- path: file,
9312
+ path: displayPath,
9301
9313
  size: stats.size
9302
9314
  });
9303
9315
  } catch {
9304
- unusedResults.push({ path: file, size: 0 });
9316
+ unusedResults.push({ path: displayPath, size: 0 });
9305
9317
  }
9306
9318
  }
9307
9319
  }
@@ -9337,7 +9349,7 @@ function resolveImport(baseDir, impPath, extensions, rootDir) {
9337
9349
  // src/scanners/unused-exports.ts
9338
9350
  var import_fast_glob3 = __toESM(require_out4(), 1);
9339
9351
  import { readFileSync as readFileSync3 } from "node:fs";
9340
- import { join as join3 } from "node:path";
9352
+ import { join as join3, relative as relative2 } from "node:path";
9341
9353
  import { Worker } from "node:worker_threads";
9342
9354
  import { fileURLToPath } from "node:url";
9343
9355
  import { dirname as dirname2 } from "node:path";
@@ -9440,42 +9452,66 @@ async function scanUnusedExports(config) {
9440
9452
  const cwd = config.dir;
9441
9453
  const extensions = config.extensions;
9442
9454
  const extGlob = `**/*{${extensions.join(",")}}`;
9443
- const allFiles = await import_fast_glob3.default(extGlob, {
9444
- cwd,
9445
- ignore: [...config.ignore.folders, ...config.ignore.files]
9455
+ const candidateCwd = config.appSpecificScan ? config.appSpecificScan.appDir : cwd;
9456
+ const referenceCwd = config.appSpecificScan ? config.appSpecificScan.rootDir : cwd;
9457
+ console.log(`
9458
+ \uD83D\uDD0D Finding export candidates in: ${candidateCwd}`);
9459
+ console.log(` \uD83C\uDF0D Checking usage in global scope: ${referenceCwd}
9460
+ `);
9461
+ const candidateFiles = await import_fast_glob3.default(extGlob, {
9462
+ cwd: candidateCwd,
9463
+ ignore: [...config.ignore.folders, ...config.ignore.files],
9464
+ absolute: true
9446
9465
  });
9447
- if (allFiles.length === 0) {
9466
+ if (candidateFiles.length === 0) {
9448
9467
  return { total: 0, used: 0, unused: 0, exports: [] };
9449
9468
  }
9469
+ const referenceFiles = await import_fast_glob3.default(extGlob, {
9470
+ cwd: referenceCwd,
9471
+ ignore: [...config.ignore.folders, ...config.ignore.files],
9472
+ absolute: true
9473
+ });
9450
9474
  const exportMap = new Map;
9451
9475
  const totalContents = new Map;
9452
9476
  let allExportsCount = 0;
9453
9477
  const inlineExportRegex = /^export\s+(?:async\s+)?(?:const|let|var|function|type|interface|enum|class)\s+([a-zA-Z0-9_$]+)/gm;
9454
9478
  const blockExportRegex = /^export\s*\{([^}]+)\}/gm;
9455
- const USE_WORKERS = allFiles.length >= 500;
9479
+ const USE_WORKERS = referenceFiles.length >= 500;
9456
9480
  const WORKER_COUNT = 2;
9457
9481
  if (USE_WORKERS) {
9458
- console.log(`\uD83D\uDCDD Scanning ${allFiles.length} files for exports (using ${WORKER_COUNT} workers)...`);
9459
- const result = await processFilesInParallel(allFiles, cwd, WORKER_COUNT);
9460
- for (const [file, exports] of result.exportMap.entries()) {
9461
- exportMap.set(file, exports);
9462
- allExportsCount += exports.length;
9463
- }
9482
+ console.log(`\uD83D\uDCDD Scanning ${candidateFiles.length} candidate files for exports & ${referenceFiles.length} files for usage...`);
9483
+ const result = await processFilesInParallel(referenceFiles, referenceCwd, WORKER_COUNT);
9464
9484
  for (const [file, content] of result.contents.entries()) {
9465
9485
  totalContents.set(file, content);
9466
9486
  }
9487
+ const candidateSet = new Set(candidateFiles);
9488
+ for (const [file, exports] of result.exportMap.entries()) {
9489
+ const absoluteFile = file.startsWith("/") ? file : join3(referenceCwd, file);
9490
+ if (candidateSet.has(absoluteFile)) {
9491
+ const displayPath = relative2(config.dir, absoluteFile);
9492
+ const mappedExports = exports.map((e) => ({ ...e, file: displayPath }));
9493
+ exportMap.set(displayPath, mappedExports);
9494
+ allExportsCount += mappedExports.length;
9495
+ }
9496
+ }
9467
9497
  } else {
9468
- console.log(`\uD83D\uDCDD Scanning ${allFiles.length} files for exports...`);
9498
+ console.log(`\uD83D\uDCDD Scanning ${candidateFiles.length} files for exports...`);
9499
+ for (const file of referenceFiles) {
9500
+ try {
9501
+ const content = readFileSync3(file, "utf-8");
9502
+ totalContents.set(file, content);
9503
+ } catch {}
9504
+ }
9469
9505
  let processedFiles = 0;
9470
- for (const file of allFiles) {
9506
+ for (const file of candidateFiles) {
9471
9507
  try {
9472
9508
  processedFiles++;
9473
- if (processedFiles % 10 === 0 || processedFiles === allFiles.length) {
9474
- const percent = Math.round(processedFiles / allFiles.length * 100);
9475
- const shortFile = file.length > 50 ? "..." + file.slice(-47) : file;
9476
- process.stdout.write(`\r Progress: ${processedFiles}/${allFiles.length} (${percent}%) - ${shortFile}${" ".repeat(10)}`);
9509
+ const displayPath = relative2(config.dir, file);
9510
+ if (processedFiles % 10 === 0 || processedFiles === candidateFiles.length) {
9511
+ const percent = Math.round(processedFiles / candidateFiles.length * 100);
9512
+ process.stdout.write(`\r Progress: ${processedFiles}/${candidateFiles.length} (${percent}%)`);
9477
9513
  }
9478
- const content = readFileSync3(join3(cwd, file), "utf-8");
9514
+ const content = totalContents.get(file) || readFileSync3(file, "utf-8");
9479
9515
  totalContents.set(file, content);
9480
9516
  const isService = file.endsWith(".service.ts") || file.endsWith(".service.tsx");
9481
9517
  const lines = content.split(`
@@ -9485,7 +9521,7 @@ async function scanUnusedExports(config) {
9485
9521
  inlineExportRegex.lastIndex = 0;
9486
9522
  let match2;
9487
9523
  while ((match2 = inlineExportRegex.exec(line)) !== null) {
9488
- if (addExport(file, match2[1], i + 1)) {
9524
+ if (addExport(displayPath, match2[1], i + 1)) {
9489
9525
  allExportsCount++;
9490
9526
  }
9491
9527
  }
@@ -9496,7 +9532,7 @@ async function scanUnusedExports(config) {
9496
9532
  return parts[parts.length - 1];
9497
9533
  });
9498
9534
  for (const name of names) {
9499
- if (addExport(file, name, i + 1)) {
9535
+ if (addExport(displayPath, name, i + 1)) {
9500
9536
  allExportsCount++;
9501
9537
  }
9502
9538
  }
@@ -9506,9 +9542,9 @@ async function scanUnusedExports(config) {
9506
9542
  while ((match2 = classMethodRegex.exec(line)) !== null) {
9507
9543
  const name = match2[1];
9508
9544
  if (name && !NEST_LIFECYCLE_METHODS.has(name) && !IGNORED_EXPORT_NAMES.has(name)) {
9509
- const existing = exportMap.get(file)?.find((e) => e.name === name);
9545
+ const existing = exportMap.get(displayPath)?.find((e) => e.name === name);
9510
9546
  if (!existing) {
9511
- if (addExport(file, name, i + 1)) {
9547
+ if (addExport(displayPath, name, i + 1)) {
9512
9548
  allExportsCount++;
9513
9549
  }
9514
9550
  }
@@ -9903,7 +9939,7 @@ async function scan(config) {
9903
9939
  // src/config.ts
9904
9940
  var import_fast_glob5 = __toESM(require_out4(), 1);
9905
9941
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
9906
- import { join as join5, resolve as resolve2, relative as relative2, dirname as dirname3 } from "node:path";
9942
+ import { join as join5, resolve as resolve2, relative as relative3, dirname as dirname3 } from "node:path";
9907
9943
  var DEFAULT_CONFIG = {
9908
9944
  dir: "./",
9909
9945
  ignore: {
@@ -9977,7 +10013,7 @@ function loadConfig(options) {
9977
10013
  const content = readFileSync5(configPath, "utf-8");
9978
10014
  const config = JSON.parse(content);
9979
10015
  const configDir = dirname3(configPath);
9980
- const relDir = relative2(cwd, configDir);
10016
+ const relDir = relative3(cwd, configDir);
9981
10017
  const prefixPattern = (p) => {
9982
10018
  if (p.startsWith("**/") || p.startsWith("/") || !relDir)
9983
10019
  return p;
@@ -10262,34 +10298,66 @@ program2.command("init").description("Create a default pruny.config.json file").
10262
10298
  program2.action(async (options) => {
10263
10299
  const startTime = Date.now();
10264
10300
  try {
10265
- const config = loadConfig({
10301
+ const baseConfig = loadConfig({
10266
10302
  dir: options.dir,
10267
10303
  config: options.config,
10268
10304
  excludePublic: !options.public
10269
10305
  });
10270
- const absoluteDir = config.dir.startsWith("/") ? config.dir : join8(process.cwd(), config.dir);
10271
- config.dir = absoluteDir;
10306
+ const absoluteDir = baseConfig.dir.startsWith("/") ? baseConfig.dir : join8(process.cwd(), baseConfig.dir);
10307
+ baseConfig.dir = absoluteDir;
10272
10308
  if (options.verbose)
10273
10309
  console.log("");
10274
10310
  console.log(source_default.bold(`
10275
10311
  \uD83D\uDD0D Scanning for unused API routes...
10276
10312
  `));
10277
- let result = await scan(config);
10278
- logScanStats(result);
10279
- if (options.filter) {
10280
- filterResults(result, options.filter);
10281
- }
10282
- if (options.json) {
10283
- console.log(JSON.stringify(result, null, 2));
10284
- return;
10285
- }
10286
- if (options.fix) {
10287
- await handleFixes(result, config, options);
10288
- } else if (hasUnusedItems(result)) {
10289
- console.log(source_default.dim(`\uD83D\uDCA1 Run with --fix to automatically clean up unused routes, files, and exports.
10313
+ const appsDir = join8(absoluteDir, "apps");
10314
+ const isMonorepo = existsSync7(appsDir) && lstatSync(appsDir).isDirectory();
10315
+ const appsToScan = [];
10316
+ if (isMonorepo) {
10317
+ const apps = readdirSync(appsDir);
10318
+ for (const app of apps) {
10319
+ const appPath = join8(appsDir, app);
10320
+ if (lstatSync(appPath).isDirectory()) {
10321
+ appsToScan.push(app);
10322
+ }
10323
+ }
10324
+ console.log(source_default.bold(`
10325
+ \uD83C\uDFE2 Monorepo Detected. Found ${appsToScan.length} apps: ${appsToScan.join(", ")}
10290
10326
  `));
10327
+ } else {
10328
+ appsToScan.push("root");
10329
+ }
10330
+ for (const appName of appsToScan) {
10331
+ const currentConfig = { ...baseConfig };
10332
+ let appLabel = "Root App";
10333
+ let appDir = absoluteDir;
10334
+ if (isMonorepo) {
10335
+ appLabel = `App: ${appName}`;
10336
+ appDir = join8(absoluteDir, "apps", appName);
10337
+ currentConfig.appSpecificScan = {
10338
+ appDir,
10339
+ rootDir: absoluteDir
10340
+ };
10341
+ }
10342
+ console.log(source_default.bold.magenta(`
10343
+ \uD83D\uDC49 Scanning ${appLabel}...`));
10344
+ let result = await scan(currentConfig);
10345
+ logScanStats(result, appLabel);
10346
+ if (options.filter) {
10347
+ filterResults(result, options.filter);
10348
+ }
10349
+ if (options.json) {
10350
+ console.log(JSON.stringify(result, null, 2));
10351
+ } else {
10352
+ if (options.fix) {
10353
+ await handleFixes(result, currentConfig, options);
10354
+ } else if (hasUnusedItems(result)) {
10355
+ console.log(source_default.dim(`\uD83D\uDCA1 Run with --fix to clean up.
10356
+ `));
10357
+ }
10358
+ printSummaryTable(result, appLabel);
10359
+ }
10291
10360
  }
10292
- printSummaryTable(result);
10293
10361
  } catch (err) {
10294
10362
  console.error(source_default.red("Error scanning:"), err);
10295
10363
  process.exit(1);
@@ -10299,8 +10367,8 @@ program2.action(async (options) => {
10299
10367
  ⏱️ Completed in ${elapsed}s`));
10300
10368
  });
10301
10369
  program2.parse();
10302
- function logScanStats(result) {
10303
- console.log(source_default.blue.bold("\uD83D\uDCCA Scan Statistics:"));
10370
+ function logScanStats(result, context) {
10371
+ console.log(source_default.blue.bold(`\uD83D\uDCCA stats for ${context}:`));
10304
10372
  console.log(source_default.blue(` • API Routes: ${result.total}`));
10305
10373
  if (result.publicAssets) {
10306
10374
  console.log(source_default.blue(` • Public Assets: ${result.publicAssets.total}`));
@@ -10557,8 +10625,8 @@ async function fixUnusedExports(result, config) {
10557
10625
  `));
10558
10626
  return fixedSomething;
10559
10627
  }
10560
- function printSummaryTable(result) {
10561
- console.log(source_default.bold(`\uD83D\uDCCA Summary Report
10628
+ function printSummaryTable(result, context) {
10629
+ console.log(source_default.bold(`\uD83D\uDCCA Summary Report for ${context}
10562
10630
  `));
10563
10631
  const summary = [];
10564
10632
  const groupedRoutes = new Map;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Find and remove unused Next.js API routes & Nest.js Controllers",
5
5
  "type": "module",
6
6
  "files": [