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.
- package/dist/index.js +153 -19
- 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
|
|
7635
|
+
import { dirname, join as join5 } from "node:path";
|
|
7636
7636
|
|
|
7637
7637
|
// src/scanner.ts
|
|
7638
|
-
var
|
|
7639
|
-
import { existsSync as existsSync2, readFileSync as
|
|
7640
|
-
import { join as
|
|
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 =
|
|
9342
|
+
const vercelPath = join3(dir, "vercel.json");
|
|
9239
9343
|
if (!existsSync2(vercelPath)) {
|
|
9240
9344
|
return [];
|
|
9241
9345
|
}
|
|
9242
9346
|
try {
|
|
9243
|
-
const content =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
9396
|
+
const filePath = join3(cwd, file);
|
|
9293
9397
|
try {
|
|
9294
|
-
const content =
|
|
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
|
|
9339
|
-
import { join as
|
|
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: [
|
|
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 =
|
|
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 =
|
|
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 :
|
|
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(
|
|
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}`));
|