pruny 1.1.20 → 1.1.23

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 +153 -19
  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, join as join4 } from "node:path";
7635
+ import { dirname, join as join5 } from "node:path";
7636
7636
 
7637
7637
  // src/scanner.ts
7638
- var import_fast_glob2 = __toESM(require_out4(), 1);
7639
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
7640
- import { join as join2 } from "node:path";
7638
+ var import_fast_glob3 = __toESM(require_out4(), 1);
7639
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
7640
+ import { join as join3 } 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;
@@ -9177,6 +9177,110 @@ async function scanPublicAssets(config) {
9177
9177
  };
9178
9178
  }
9179
9179
 
9180
+ // src/scanners/unused-files.ts
9181
+ var import_fast_glob2 = __toESM(require_out4(), 1);
9182
+ import { readFileSync as readFileSync2, statSync } from "node:fs";
9183
+ import { join as join2 } from "node:path";
9184
+ async function scanUnusedFiles(config) {
9185
+ const cwd = config.dir;
9186
+ const extensions = config.extensions;
9187
+ const extGlob = `**/*{${extensions.join(",")}}`;
9188
+ const allFiles = await import_fast_glob2.default(extGlob, {
9189
+ cwd,
9190
+ ignore: [...config.ignore.folders, ...config.ignore.files]
9191
+ });
9192
+ if (allFiles.length === 0) {
9193
+ return { total: 0, files: [] };
9194
+ }
9195
+ const entryFiles = new Set;
9196
+ const entryPatterns = [
9197
+ "**/page.{ts,tsx,js,jsx}",
9198
+ "**/layout.{ts,tsx,js,jsx}",
9199
+ "**/route.{ts,tsx,js,jsx}",
9200
+ "**/loading.{ts,tsx,js,jsx}",
9201
+ "**/error.{ts,tsx,js,jsx}",
9202
+ "**/not-found.{ts,tsx,js,jsx}",
9203
+ "**/middleware.{ts,js}",
9204
+ "**/instrumentation.{ts,js}",
9205
+ "next.config.{js,mjs,ts}",
9206
+ "tailwind.config.{js,ts}",
9207
+ "postcss.config.{js,ts}",
9208
+ "app/api/**",
9209
+ "app/robots.ts",
9210
+ "app/sitemap.ts",
9211
+ "next-sitemap.config.js",
9212
+ "cypress.config.ts",
9213
+ "env.d.ts",
9214
+ "next-env.d.ts",
9215
+ "**/*.d.ts",
9216
+ "scripts/**",
9217
+ "cypress/**",
9218
+ "public/sw.js"
9219
+ ];
9220
+ for (const file of allFiles) {
9221
+ const isEntry = entryPatterns.some((pattern) => {
9222
+ return minimatch(file, pattern, { dot: true });
9223
+ });
9224
+ if (isEntry)
9225
+ entryFiles.add(file);
9226
+ }
9227
+ const importedPaths = new Set;
9228
+ const importRegex = /from\s+['"]([^'"]+)['"]|import\(['"]([^'"]+)['"]\)|require\(['"]([^'"]+)['"]\)/g;
9229
+ for (const file of allFiles) {
9230
+ try {
9231
+ const content = readFileSync2(join2(cwd, file), "utf-8");
9232
+ let match2;
9233
+ while ((match2 = importRegex.exec(content)) !== null) {
9234
+ const imp = match2[1] || match2[2] || match2[3];
9235
+ if (imp && (imp.startsWith(".") || imp.startsWith("@/") || imp.startsWith("~/"))) {
9236
+ const cleanImp = imp.replace(/\.(ts|tsx|js|jsx)$/, "");
9237
+ importedPaths.add(cleanImp);
9238
+ if (cleanImp.endsWith("/")) {
9239
+ importedPaths.add(cleanImp + "index");
9240
+ } else if (!cleanImp.includes("/")) {}
9241
+ }
9242
+ }
9243
+ } catch {}
9244
+ }
9245
+ const unusedResults = [];
9246
+ for (const file of allFiles) {
9247
+ if (entryFiles.has(file))
9248
+ continue;
9249
+ const fileBase = file.replace(/\.(ts|tsx|js|jsx)$/, "");
9250
+ const fileName = file.split("/").pop()?.replace(/\.(ts|tsx|js|jsx)$/, "") || "";
9251
+ const isUsed = Array.from(importedPaths).some((imp) => {
9252
+ if (imp.endsWith(fileBase))
9253
+ return true;
9254
+ if (fileName === "index" && imp === fileBase.replace(/\/index$/, ""))
9255
+ return true;
9256
+ if (imp.startsWith("@/") || imp.startsWith("~/")) {
9257
+ const strippedFile = fileBase.replace(/^(src|app)\//, "");
9258
+ if (imp.substring(2) === strippedFile)
9259
+ return true;
9260
+ }
9261
+ if (imp.endsWith("/" + fileName))
9262
+ return true;
9263
+ return false;
9264
+ });
9265
+ if (!isUsed) {
9266
+ const fullPath = join2(cwd, file);
9267
+ try {
9268
+ const stats = statSync(fullPath);
9269
+ unusedResults.push({
9270
+ path: file,
9271
+ size: stats.size
9272
+ });
9273
+ } catch {
9274
+ unusedResults.push({ path: file, size: 0 });
9275
+ }
9276
+ }
9277
+ }
9278
+ return {
9279
+ total: unusedResults.length,
9280
+ files: unusedResults
9281
+ };
9282
+ }
9283
+
9180
9284
  // src/scanner.ts
9181
9285
  function extractRoutePath(filePath) {
9182
9286
  let path2 = filePath.replace(/^src\//, "");
@@ -9235,12 +9339,12 @@ function checkRouteUsage(routePath, references) {
9235
9339
  return { used, usedMethods };
9236
9340
  }
9237
9341
  function getVercelCronPaths(dir) {
9238
- const vercelPath = join2(dir, "vercel.json");
9342
+ const vercelPath = join3(dir, "vercel.json");
9239
9343
  if (!existsSync2(vercelPath)) {
9240
9344
  return [];
9241
9345
  }
9242
9346
  try {
9243
- const content = readFileSync2(vercelPath, "utf-8");
9347
+ const content = readFileSync3(vercelPath, "utf-8");
9244
9348
  const config = JSON.parse(content);
9245
9349
  if (!config.crons) {
9246
9350
  return [];
@@ -9256,12 +9360,12 @@ async function scan(config) {
9256
9360
  "app/api/**/route.{ts,tsx,js,jsx}",
9257
9361
  "src/app/api/**/route.{ts,tsx,js,jsx}"
9258
9362
  ];
9259
- const routeFiles = await import_fast_glob2.default(routePatterns, {
9363
+ const routeFiles = await import_fast_glob3.default(routePatterns, {
9260
9364
  cwd,
9261
9365
  ignore: config.ignore.folders
9262
9366
  });
9263
9367
  const routes = routeFiles.length > 0 ? routeFiles.map((file) => {
9264
- const content = readFileSync2(join2(cwd, file), "utf-8");
9368
+ const content = readFileSync3(join3(cwd, file), "utf-8");
9265
9369
  const methods = extractExportedMethods(content);
9266
9370
  return {
9267
9371
  path: extractRoutePath(file),
@@ -9282,16 +9386,16 @@ async function scan(config) {
9282
9386
  }
9283
9387
  }
9284
9388
  const extGlob = `**/*{${config.extensions.join(",")}}`;
9285
- const sourceFiles = await import_fast_glob2.default(extGlob, {
9389
+ const sourceFiles = await import_fast_glob3.default(extGlob, {
9286
9390
  cwd,
9287
9391
  ignore: [...config.ignore.folders, ...config.ignore.files]
9288
9392
  });
9289
9393
  const allReferences = [];
9290
9394
  const fileReferences = new Map;
9291
9395
  for (const file of sourceFiles) {
9292
- const filePath = join2(cwd, file);
9396
+ const filePath = join3(cwd, file);
9293
9397
  try {
9294
- const content = readFileSync2(filePath, "utf-8");
9398
+ const content = readFileSync3(filePath, "utf-8");
9295
9399
  const refs = extractApiReferences(content);
9296
9400
  if (refs.length > 0) {
9297
9401
  fileReferences.set(file, refs);
@@ -9325,24 +9429,40 @@ async function scan(config) {
9325
9429
  if (!config.excludePublic) {
9326
9430
  publicAssets = await scanPublicAssets(config);
9327
9431
  }
9432
+ const unusedFiles = await scanUnusedFiles(config);
9328
9433
  return {
9329
9434
  total: routes.length,
9330
9435
  used: routes.filter((r) => r.used).length,
9331
9436
  unused: routes.filter((r) => !r.used).length,
9332
9437
  routes,
9333
- publicAssets
9438
+ publicAssets,
9439
+ unusedFiles
9334
9440
  };
9335
9441
  }
9336
9442
 
9337
9443
  // src/config.ts
9338
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
9339
- import { join as join3 } from "node:path";
9444
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
9445
+ import { join as join4 } from "node:path";
9340
9446
  var DEFAULT_CONFIG = {
9341
9447
  dir: "./",
9342
9448
  ignore: {
9343
9449
  routes: [],
9344
9450
  folders: ["node_modules", ".next", "dist", ".git", "coverage", ".turbo"],
9345
- files: ["*.test.ts", "*.spec.ts", "*.test.tsx", "*.spec.tsx"]
9451
+ files: [
9452
+ "*.test.ts",
9453
+ "*.spec.ts",
9454
+ "*.test.tsx",
9455
+ "*.spec.tsx",
9456
+ "public/robots.txt",
9457
+ "public/sitemap*.xml",
9458
+ "public/favicon.ico",
9459
+ "public/sw.js",
9460
+ "public/manifest.json",
9461
+ "public/twitter-image.*",
9462
+ "public/opengraph-image.*",
9463
+ "public/apple-icon.*",
9464
+ "public/icon.*"
9465
+ ]
9346
9466
  },
9347
9467
  extensions: [".ts", ".tsx", ".js", ".jsx"]
9348
9468
  };
@@ -9351,7 +9471,7 @@ function loadConfig(options) {
9351
9471
  let fileConfig = {};
9352
9472
  if (configPath && existsSync3(configPath)) {
9353
9473
  try {
9354
- const content = readFileSync3(configPath, "utf-8");
9474
+ const content = readFileSync4(configPath, "utf-8");
9355
9475
  fileConfig = JSON.parse(content);
9356
9476
  } catch {}
9357
9477
  }
@@ -9378,7 +9498,7 @@ function loadConfig(options) {
9378
9498
  function findConfigFile(dir) {
9379
9499
  const candidates = ["pruny.config.json", ".prunyrc.json", ".prunyrc"];
9380
9500
  for (const name of candidates) {
9381
- const path2 = join3(dir, name);
9501
+ const path2 = join4(dir, name);
9382
9502
  if (existsSync3(path2)) {
9383
9503
  return path2;
9384
9504
  }
@@ -9394,7 +9514,7 @@ program2.name("pruny").description("Find and remove unused Next.js API routes").
9394
9514
  config: options.config,
9395
9515
  excludePublic: !options.public
9396
9516
  });
9397
- const absoluteDir = config.dir.startsWith("/") ? config.dir : join4(process.cwd(), config.dir);
9517
+ const absoluteDir = config.dir.startsWith("/") ? config.dir : join5(process.cwd(), config.dir);
9398
9518
  config.dir = absoluteDir;
9399
9519
  if (options.verbose) {
9400
9520
  console.log(source_default.dim(`
@@ -9424,6 +9544,11 @@ Config:`));
9424
9544
  console.log(source_default.green(` Used assets: ${result.publicAssets.used}`));
9425
9545
  console.log(source_default.red(` Unused assets: ${result.publicAssets.unused}`));
9426
9546
  }
9547
+ if (result.unusedFiles) {
9548
+ console.log("");
9549
+ console.log(source_default.bold("\uD83D\uDCC4 Source Files"));
9550
+ console.log(source_default.red(` Unused files: ${result.unusedFiles.total}`));
9551
+ }
9427
9552
  console.log("");
9428
9553
  const unusedRoutes = result.routes.filter((r) => !r.used);
9429
9554
  if (unusedRoutes.length > 0) {
@@ -9466,6 +9591,15 @@ Config:`));
9466
9591
  `));
9467
9592
  }
9468
9593
  }
9594
+ if (result.unusedFiles && result.unusedFiles.files.length > 0) {
9595
+ console.log(source_default.red.bold(`❌ Unused Source Files:
9596
+ `));
9597
+ for (const file of result.unusedFiles.files) {
9598
+ const sizeKb = (file.size / 1024).toFixed(1);
9599
+ console.log(source_default.red(` ${file.path} ${source_default.dim(`(${sizeKb} KB)`)}`));
9600
+ }
9601
+ console.log("");
9602
+ }
9469
9603
  if (options.verbose) {
9470
9604
  const used = result.routes.filter((r) => r.used);
9471
9605
  if (used.length > 0) {
@@ -9490,7 +9624,7 @@ Config:`));
9490
9624
  console.log(source_default.yellow.bold(`\uD83D\uDDD1️ Deleting unused routes...
9491
9625
  `));
9492
9626
  for (const route of unusedRoutes) {
9493
- const routeDir = dirname(join4(config.dir, route.filePath));
9627
+ const routeDir = dirname(join5(config.dir, route.filePath));
9494
9628
  try {
9495
9629
  rmSync(routeDir, { recursive: true, force: true });
9496
9630
  console.log(source_default.red(` Deleted: ${route.filePath}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.1.20",
3
+ "version": "1.1.23",
4
4
  "description": "Find and remove unused Next.js API routes",
5
5
  "type": "module",
6
6
  "files": [