pruny 1.9.2 → 1.10.1

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
@@ -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 join8 } from "node:path";
7635
+ import { dirname as dirname3, join as join8 } from "node:path";
7636
7636
 
7637
7637
  // src/scanner.ts
7638
7638
  var import_fast_glob4 = __toESM(require_out4(), 1);
@@ -9249,6 +9249,13 @@ async function scanUnusedFiles(config) {
9249
9249
  } else if (imp.startsWith("@/") || imp.startsWith("~/")) {
9250
9250
  const aliasPath = imp.substring(2);
9251
9251
  resolvedFile = resolveImport(cwd, aliasPath, extensions, cwd) || resolveImport(join2(cwd, "src"), aliasPath, extensions, cwd) || resolveImport(join2(cwd, "app"), aliasPath, extensions, cwd);
9252
+ if (!resolvedFile) {
9253
+ const pathParts = currentFile.split(/[/\\]/);
9254
+ if (pathParts.length >= 2 && (pathParts[0] === "apps" || pathParts[0] === "packages")) {
9255
+ const projectRoot = join2(cwd, pathParts[0], pathParts[1]);
9256
+ resolvedFile = resolveImport(projectRoot, aliasPath, extensions, cwd) || resolveImport(join2(projectRoot, "src"), aliasPath, extensions, cwd) || resolveImport(join2(projectRoot, "app"), aliasPath, extensions, cwd);
9257
+ }
9258
+ }
9252
9259
  }
9253
9260
  if (resolvedFile && allFilesSet.has(resolvedFile)) {
9254
9261
  usedFiles.add(resolvedFile);
@@ -9308,6 +9315,9 @@ function resolveImport(baseDir, impPath, extensions, rootDir) {
9308
9315
  var import_fast_glob3 = __toESM(require_out4(), 1);
9309
9316
  import { readFileSync as readFileSync3 } from "node:fs";
9310
9317
  import { join as join3 } from "node:path";
9318
+ import { Worker } from "node:worker_threads";
9319
+ import { fileURLToPath } from "node:url";
9320
+ import { dirname as dirname2 } from "node:path";
9311
9321
  var IGNORED_EXPORT_NAMES = new Set([
9312
9322
  "config",
9313
9323
  "generateMetadata",
@@ -9336,6 +9346,71 @@ var IGNORED_EXPORT_NAMES = new Set([
9336
9346
  "OPTIONS",
9337
9347
  "default"
9338
9348
  ]);
9349
+ async function processFilesInParallel(files, cwd, workerCount) {
9350
+ const __filename2 = fileURLToPath(import.meta.url);
9351
+ const __dirname2 = dirname2(__filename2);
9352
+ const workerPath = join3(__dirname2, "workers/file-processor.js");
9353
+ const chunkSize = Math.ceil(files.length / workerCount);
9354
+ const chunks = [];
9355
+ for (let i = 0;i < workerCount; i++) {
9356
+ const start = i * chunkSize;
9357
+ const end = Math.min(start + chunkSize, files.length);
9358
+ if (start < files.length) {
9359
+ chunks.push(files.slice(start, end));
9360
+ }
9361
+ }
9362
+ const exportMap = new Map;
9363
+ const contents = new Map;
9364
+ const progressMap = new Map;
9365
+ const workerPromises = chunks.map((chunk, chunkId) => {
9366
+ return new Promise((resolve2, reject) => {
9367
+ const worker = new Worker(workerPath, {
9368
+ workerData: {
9369
+ files: chunk,
9370
+ cwd,
9371
+ chunkId
9372
+ }
9373
+ });
9374
+ worker.on("message", (msg) => {
9375
+ if (msg.type === "progress") {
9376
+ progressMap.set(msg.chunkId, {
9377
+ processed: msg.processed,
9378
+ total: msg.total
9379
+ });
9380
+ let totalProcessed = 0;
9381
+ let totalFiles = 0;
9382
+ for (const [, progress] of progressMap.entries()) {
9383
+ totalProcessed += progress.processed;
9384
+ totalFiles += progress.total;
9385
+ }
9386
+ const percent = Math.round(totalProcessed / totalFiles * 100);
9387
+ process.stdout.write(`\r Progress: ${totalProcessed}/${totalFiles} (${percent}%)...${" ".repeat(10)}`);
9388
+ } else if (msg.type === "complete") {
9389
+ const result = msg.result;
9390
+ const workerExportMap = new Map(Object.entries(result.exports));
9391
+ const workerContents = new Map(Object.entries(result.contents));
9392
+ for (const [file, exports] of workerExportMap.entries()) {
9393
+ exportMap.set(file, exports);
9394
+ }
9395
+ for (const [file, content] of workerContents.entries()) {
9396
+ contents.set(file, content);
9397
+ }
9398
+ worker.terminate();
9399
+ resolve2();
9400
+ }
9401
+ });
9402
+ worker.on("error", reject);
9403
+ worker.on("exit", (code) => {
9404
+ if (code !== 0) {
9405
+ reject(new Error(`Worker stopped with exit code ${code}`));
9406
+ }
9407
+ });
9408
+ });
9409
+ });
9410
+ await Promise.all(workerPromises);
9411
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
9412
+ return { exportMap, contents };
9413
+ }
9339
9414
  async function scanUnusedExports(config) {
9340
9415
  const cwd = config.dir;
9341
9416
  const extensions = config.extensions;
@@ -9352,46 +9427,60 @@ async function scanUnusedExports(config) {
9352
9427
  let allExportsCount = 0;
9353
9428
  const inlineExportRegex = /^export\s+(?:async\s+)?(?:const|let|var|function|type|interface|enum|class)\s+([a-zA-Z0-9_$]+)/gm;
9354
9429
  const blockExportRegex = /^export\s*\{([^}]+)\}/gm;
9355
- console.log(`\uD83D\uDCDD Scanning ${allFiles.length} files for exports...`);
9356
- let processedFiles = 0;
9357
- for (const file of allFiles) {
9358
- try {
9359
- processedFiles++;
9360
- if (processedFiles % 10 === 0 || processedFiles === allFiles.length) {
9361
- const percent = Math.round(processedFiles / allFiles.length * 100);
9362
- const shortFile = file.length > 50 ? "..." + file.slice(-47) : file;
9363
- process.stdout.write(`\r Progress: ${processedFiles}/${allFiles.length} (${percent}%) - ${shortFile}${" ".repeat(10)}`);
9364
- }
9365
- const content = readFileSync3(join3(cwd, file), "utf-8");
9430
+ const USE_WORKERS = allFiles.length >= 500;
9431
+ const WORKER_COUNT = 2;
9432
+ if (USE_WORKERS) {
9433
+ console.log(`\uD83D\uDCDD Scanning ${allFiles.length} files for exports (using ${WORKER_COUNT} workers)...`);
9434
+ const result = await processFilesInParallel(allFiles, cwd, WORKER_COUNT);
9435
+ for (const [file, exports] of result.exportMap.entries()) {
9436
+ exportMap.set(file, exports);
9437
+ allExportsCount += exports.length;
9438
+ }
9439
+ for (const [file, content] of result.contents.entries()) {
9366
9440
  totalContents.set(file, content);
9367
- const lines = content.split(`
9441
+ }
9442
+ } else {
9443
+ console.log(`\uD83D\uDCDD Scanning ${allFiles.length} files for exports...`);
9444
+ let processedFiles = 0;
9445
+ for (const file of allFiles) {
9446
+ try {
9447
+ processedFiles++;
9448
+ if (processedFiles % 10 === 0 || processedFiles === allFiles.length) {
9449
+ const percent = Math.round(processedFiles / allFiles.length * 100);
9450
+ const shortFile = file.length > 50 ? "..." + file.slice(-47) : file;
9451
+ process.stdout.write(`\r Progress: ${processedFiles}/${allFiles.length} (${percent}%) - ${shortFile}${" ".repeat(10)}`);
9452
+ }
9453
+ const content = readFileSync3(join3(cwd, file), "utf-8");
9454
+ totalContents.set(file, content);
9455
+ const lines = content.split(`
9368
9456
  `);
9369
- for (let i = 0;i < lines.length; i++) {
9370
- const line = lines[i];
9371
- inlineExportRegex.lastIndex = 0;
9372
- let match2;
9373
- while ((match2 = inlineExportRegex.exec(line)) !== null) {
9374
- if (addExport(file, match2[1], i + 1)) {
9375
- allExportsCount++;
9376
- }
9377
- }
9378
- blockExportRegex.lastIndex = 0;
9379
- while ((match2 = blockExportRegex.exec(line)) !== null) {
9380
- const names = match2[1].split(",").map((n) => {
9381
- const parts = n.trim().split(/\s+as\s+/);
9382
- return parts[parts.length - 1];
9383
- });
9384
- for (const name of names) {
9385
- if (addExport(file, name, i + 1)) {
9457
+ for (let i = 0;i < lines.length; i++) {
9458
+ const line = lines[i];
9459
+ inlineExportRegex.lastIndex = 0;
9460
+ let match2;
9461
+ while ((match2 = inlineExportRegex.exec(line)) !== null) {
9462
+ if (addExport(file, match2[1], i + 1)) {
9386
9463
  allExportsCount++;
9387
9464
  }
9388
9465
  }
9466
+ blockExportRegex.lastIndex = 0;
9467
+ while ((match2 = blockExportRegex.exec(line)) !== null) {
9468
+ const names = match2[1].split(",").map((n) => {
9469
+ const parts = n.trim().split(/\s+as\s+/);
9470
+ return parts[parts.length - 1];
9471
+ });
9472
+ for (const name of names) {
9473
+ if (addExport(file, name, i + 1)) {
9474
+ allExportsCount++;
9475
+ }
9476
+ }
9477
+ }
9389
9478
  }
9390
- }
9391
- } catch {}
9392
- }
9393
- if (processedFiles > 0) {
9394
- process.stdout.write("\r" + " ".repeat(60) + "\r");
9479
+ } catch {}
9480
+ }
9481
+ if (processedFiles > 0) {
9482
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
9483
+ }
9395
9484
  }
9396
9485
  function addExport(file, name, line) {
9397
9486
  if (name && !IGNORED_EXPORT_NAMES.has(name)) {
@@ -9412,17 +9501,34 @@ async function scanUnusedExports(config) {
9412
9501
  if (fileContent) {
9413
9502
  const lines = fileContent.split(`
9414
9503
  `);
9504
+ let fileInMultilineComment = false;
9505
+ let fileInTemplateLiteral = false;
9415
9506
  for (let i = 0;i < lines.length; i++) {
9416
9507
  if (i === exp.line - 1)
9417
9508
  continue;
9418
9509
  const line = lines[i];
9419
9510
  const trimmed = line.trim();
9420
- if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*"))
9511
+ if (trimmed.includes("/*"))
9512
+ fileInMultilineComment = true;
9513
+ if (trimmed.includes("*/")) {
9514
+ fileInMultilineComment = false;
9515
+ continue;
9516
+ }
9517
+ if (fileInMultilineComment)
9518
+ continue;
9519
+ const backtickCount = (line.match(/`/g) || []).length;
9520
+ if (backtickCount % 2 !== 0) {
9521
+ fileInTemplateLiteral = !fileInTemplateLiteral;
9522
+ }
9523
+ if (fileInTemplateLiteral)
9421
9524
  continue;
9525
+ if (trimmed.startsWith("//"))
9526
+ continue;
9527
+ const lineWithoutStrings = line.replace(/'[^']*'/g, "''").replace(/"[^"]*"/g, '""');
9422
9528
  const referenceRegex = new RegExp(`\\b${exp.name}\\b`);
9423
- if (referenceRegex.test(line)) {
9424
- const codePattern = new RegExp(`\\b${exp.name}\\s*[({.,;)]|\\b${exp.name}\\s*\\)|\\s+${exp.name}\\b`);
9425
- if (codePattern.test(line)) {
9529
+ if (referenceRegex.test(lineWithoutStrings)) {
9530
+ const codePattern = new RegExp(`\\b${exp.name}\\s*[({.,;<>|&)]|\\b${exp.name}\\s*\\)|\\s+${exp.name}\\b`);
9531
+ if (codePattern.test(lineWithoutStrings)) {
9426
9532
  usedInternally = true;
9427
9533
  break;
9428
9534
  }
@@ -9448,7 +9554,8 @@ async function scanUnusedExports(config) {
9448
9554
  `);
9449
9555
  let inMultilineComment = false;
9450
9556
  let inTemplateLiteral = false;
9451
- for (const line of lines) {
9557
+ for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) {
9558
+ const line = lines[lineIndex];
9452
9559
  const trimmed = line.trim();
9453
9560
  if (trimmed.includes("/*"))
9454
9561
  inMultilineComment = true;
@@ -9466,11 +9573,11 @@ async function scanUnusedExports(config) {
9466
9573
  continue;
9467
9574
  if (trimmed.startsWith("//"))
9468
9575
  continue;
9469
- if (trimmed.includes("{/*") || trimmed.includes("*/}"))
9470
- continue;
9471
- if (wordBoundaryPattern.test(line)) {
9472
- const codePattern = new RegExp(`\\b${exp.name}\\s*[({.,;)]|\\b${exp.name}\\s*\\)|\\s+${exp.name}\\b`);
9473
- if (codePattern.test(line)) {
9576
+ const lineWithoutStrings = line.replace(/'[^']*'/g, "''").replace(/"[^"]*"/g, '""');
9577
+ if (wordBoundaryPattern.test(lineWithoutStrings)) {
9578
+ const codePattern = new RegExp(`\\b${exp.name}\\s*[({.,;<>|&)]|\\b${exp.name}\\s*\\)|\\s+${exp.name}\\b`);
9579
+ const isMatch = codePattern.test(lineWithoutStrings);
9580
+ if (isMatch) {
9474
9581
  isUsed = true;
9475
9582
  break;
9476
9583
  }
@@ -10159,7 +10266,7 @@ program2.action(async (options) => {
10159
10266
  console.log(source_default.yellow.bold(`\uD83D\uDDD1️ Deleting unused routes...
10160
10267
  `));
10161
10268
  for (const route of unusedRoutes) {
10162
- const routeDir = dirname2(join8(config.dir, route.filePath));
10269
+ const routeDir = dirname3(join8(config.dir, route.filePath));
10163
10270
  try {
10164
10271
  rmSync(routeDir, { recursive: true, force: true });
10165
10272
  console.log(source_default.red(` Deleted: ${route.filePath}`));
@@ -0,0 +1,93 @@
1
+ // src/workers/file-processor.ts
2
+ import { parentPort, workerData } from "node:worker_threads";
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ var IGNORED_EXPORT_NAMES = new Set([
6
+ "metadata",
7
+ "viewport",
8
+ "generateMetadata",
9
+ "generateViewport",
10
+ "generateStaticParams",
11
+ "generateImageMetadata",
12
+ "generateSitemaps",
13
+ "dynamic",
14
+ "dynamicParams",
15
+ "revalidate",
16
+ "fetchCache",
17
+ "runtime",
18
+ "preferredRegion",
19
+ "maxDuration",
20
+ "config",
21
+ "GET",
22
+ "POST",
23
+ "PUT",
24
+ "PATCH",
25
+ "DELETE",
26
+ "HEAD",
27
+ "OPTIONS",
28
+ "default"
29
+ ]);
30
+ var inlineExportRegex = /^export\s+(?:async\s+)?(?:const|let|var|function|type|interface|enum|class)\s+([a-zA-Z0-9_$]+)/gm;
31
+ var blockExportRegex = /^export\s*\{([^}]+)\}/gm;
32
+ if (parentPort && workerData) {
33
+ const { files, cwd, chunkId } = workerData;
34
+ const exportMap = new Map;
35
+ const contents = new Map;
36
+ let processedCount = 0;
37
+ for (const file of files) {
38
+ try {
39
+ const content = readFileSync(join(cwd, file), "utf-8");
40
+ contents.set(file, content);
41
+ const lines = content.split(`
42
+ `);
43
+ for (let i = 0;i < lines.length; i++) {
44
+ const line = lines[i];
45
+ inlineExportRegex.lastIndex = 0;
46
+ let match;
47
+ while ((match = inlineExportRegex.exec(line)) !== null) {
48
+ const name = match[1];
49
+ if (name && !IGNORED_EXPORT_NAMES.has(name)) {
50
+ if (!exportMap.has(file))
51
+ exportMap.set(file, []);
52
+ exportMap.get(file).push({ name, line: i + 1, file });
53
+ }
54
+ }
55
+ blockExportRegex.lastIndex = 0;
56
+ while ((match = blockExportRegex.exec(line)) !== null) {
57
+ const names = match[1].split(",").map((n) => {
58
+ const parts = n.trim().split(/\s+as\s+/);
59
+ return parts[parts.length - 1];
60
+ });
61
+ for (const name of names) {
62
+ if (name && !IGNORED_EXPORT_NAMES.has(name)) {
63
+ if (!exportMap.has(file))
64
+ exportMap.set(file, []);
65
+ exportMap.get(file).push({ name, line: i + 1, file });
66
+ }
67
+ }
68
+ }
69
+ }
70
+ processedCount++;
71
+ if (processedCount % 10 === 0) {
72
+ parentPort.postMessage({
73
+ type: "progress",
74
+ chunkId,
75
+ processed: processedCount,
76
+ total: files.length
77
+ });
78
+ }
79
+ } catch {
80
+ processedCount++;
81
+ }
82
+ }
83
+ const result = {
84
+ chunkId,
85
+ exports: Object.fromEntries(exportMap),
86
+ contents: Object.fromEntries(contents),
87
+ processedCount
88
+ };
89
+ parentPort.postMessage({
90
+ type: "complete",
91
+ result
92
+ });
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.9.2",
3
+ "version": "1.10.1",
4
4
  "description": "Find and remove unused Next.js API routes & Nest.js Controllers",
5
5
  "type": "module",
6
6
  "files": [
@@ -14,7 +14,7 @@
14
14
  "main": "./dist/index.js",
15
15
  "types": "./dist/index.d.ts",
16
16
  "scripts": {
17
- "build": "bun build ./src/index.ts --outdir ./dist --target node",
17
+ "build": "bun build ./src/index.ts --outdir ./dist --target node && mkdir -p dist/workers && bun build ./src/workers/file-processor.ts --outdir ./dist/workers --target node",
18
18
  "dev": "bun run ./src/index.ts",
19
19
  "lint": "eslint src/**",
20
20
  "audit": "bun run build && node dist/index.js",