pruny 1.43.10 → 1.44.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.
- package/dist/index.js +675 -21
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -12564,7 +12564,7 @@ var source_default = chalk;
|
|
|
12564
12564
|
// src/index.ts
|
|
12565
12565
|
var import_prompts = __toESM(require_prompts3(), 1);
|
|
12566
12566
|
import { rmSync, existsSync as existsSync11, readdirSync, lstatSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
12567
|
-
import { dirname as
|
|
12567
|
+
import { dirname as dirname7, join as join11, relative as relative5, resolve as resolve5 } from "node:path";
|
|
12568
12568
|
|
|
12569
12569
|
// src/scanner.ts
|
|
12570
12570
|
var import_fast_glob10 = __toESM(require_out4(), 1);
|
|
@@ -15886,8 +15886,8 @@ async function scanUnusedServices(config) {
|
|
|
15886
15886
|
|
|
15887
15887
|
// src/scanners/broken-links.ts
|
|
15888
15888
|
var import_fast_glob9 = __toESM(require_out4(), 1);
|
|
15889
|
-
import { readFileSync as readFileSync9, existsSync as existsSync7 } from "node:fs";
|
|
15890
|
-
import { join as join7 } from "node:path";
|
|
15889
|
+
import { readFileSync as readFileSync9, existsSync as existsSync7, statSync as statSync2 } from "node:fs";
|
|
15890
|
+
import { dirname as dirname5, join as join7, resolve as resolve3 } from "node:path";
|
|
15891
15891
|
var LINK_PATTERNS = [
|
|
15892
15892
|
/<Link\s+[^>]*href\s*=\s*(?:\{\s*)?['"`](\/[^'"`\s]+)['"`](?:\s*\})?/g,
|
|
15893
15893
|
/router\.(push|replace)\s*\(\s*['"`](\/[^'"`\s]+)['"`]/g,
|
|
@@ -15944,15 +15944,15 @@ function filePathToRoute(filePath) {
|
|
|
15944
15944
|
});
|
|
15945
15945
|
return "/" + segments.join("/");
|
|
15946
15946
|
}
|
|
15947
|
-
function matchesRoute(refPath, routes,
|
|
15947
|
+
function matchesRoute(refPath, routes, routes_) {
|
|
15948
15948
|
const cleaned = cleanPath(refPath);
|
|
15949
15949
|
if (routes.has(cleaned))
|
|
15950
15950
|
return true;
|
|
15951
15951
|
const refSegments = cleaned.split("/").filter(Boolean);
|
|
15952
|
-
for (const
|
|
15953
|
-
if (matchSegments(refSegments,
|
|
15952
|
+
for (const route of routes_) {
|
|
15953
|
+
if (matchSegments(refSegments, route.segments, route.params))
|
|
15954
15954
|
return true;
|
|
15955
|
-
if (matchesDynamicSuffix(refSegments,
|
|
15955
|
+
if (matchesDynamicSuffix(refSegments, route.segments))
|
|
15956
15956
|
return true;
|
|
15957
15957
|
}
|
|
15958
15958
|
return false;
|
|
@@ -15970,14 +15970,29 @@ function matchesDynamicSuffix(refSegments, routeSegments) {
|
|
|
15970
15970
|
return false;
|
|
15971
15971
|
return matchSegments(refSegments, tail);
|
|
15972
15972
|
}
|
|
15973
|
-
function matchSegments(refSegments, routeSegments) {
|
|
15973
|
+
function matchSegments(refSegments, routeSegments, params) {
|
|
15974
15974
|
let ri = 0;
|
|
15975
15975
|
let si = 0;
|
|
15976
15976
|
while (ri < refSegments.length && si < routeSegments.length) {
|
|
15977
15977
|
const routeSeg = routeSegments[si];
|
|
15978
15978
|
if (/^\[\[?\.\.\./.test(routeSeg))
|
|
15979
15979
|
return true;
|
|
15980
|
-
|
|
15980
|
+
const dynMatch = /^\[(.+)\]$/.exec(routeSeg);
|
|
15981
|
+
if (dynMatch) {
|
|
15982
|
+
const paramName = dynMatch[1];
|
|
15983
|
+
if (params && params[paramName]) {
|
|
15984
|
+
const ref = refSegments[ri];
|
|
15985
|
+
const allowed = params[paramName];
|
|
15986
|
+
let ok = false;
|
|
15987
|
+
for (const v of allowed) {
|
|
15988
|
+
if (v.toLowerCase() === ref.toLowerCase()) {
|
|
15989
|
+
ok = true;
|
|
15990
|
+
break;
|
|
15991
|
+
}
|
|
15992
|
+
}
|
|
15993
|
+
if (!ok)
|
|
15994
|
+
return false;
|
|
15995
|
+
}
|
|
15981
15996
|
ri++;
|
|
15982
15997
|
si++;
|
|
15983
15998
|
continue;
|
|
@@ -16030,12 +16045,21 @@ async function scanBrokenLinks(config) {
|
|
|
16030
16045
|
const knownRoutes = new Set;
|
|
16031
16046
|
const routeSegmentsList = [];
|
|
16032
16047
|
knownRoutes.add("/");
|
|
16048
|
+
const aliasMap = parseTsConfigPaths(appDir);
|
|
16033
16049
|
for (const file of pageFiles) {
|
|
16034
16050
|
const route = filePathToRoute(file);
|
|
16035
16051
|
knownRoutes.add(route);
|
|
16036
16052
|
const segments = route.split("/").filter(Boolean);
|
|
16037
16053
|
if (segments.some((s) => s.startsWith("["))) {
|
|
16038
|
-
|
|
16054
|
+
const absFile = join7(appDir, file);
|
|
16055
|
+
const params = resolveStaticParams(absFile, appDir, aliasMap);
|
|
16056
|
+
routeSegmentsList.push({ segments, params: params ?? undefined });
|
|
16057
|
+
if (process.env.DEBUG_PRUNY) {
|
|
16058
|
+
if (params) {
|
|
16059
|
+
const summary = Object.entries(params).map(([k, v]) => `${k}=${v.size}`).join(", ");
|
|
16060
|
+
console.log(`[DEBUG] Static params for ${route}: ${summary}`);
|
|
16061
|
+
}
|
|
16062
|
+
}
|
|
16039
16063
|
}
|
|
16040
16064
|
}
|
|
16041
16065
|
if (process.env.DEBUG_PRUNY) {
|
|
@@ -16129,6 +16153,632 @@ async function scanBrokenLinks(config) {
|
|
|
16129
16153
|
links
|
|
16130
16154
|
};
|
|
16131
16155
|
}
|
|
16156
|
+
var TS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
16157
|
+
var JSON_EXT = ".json";
|
|
16158
|
+
function resolveStaticParams(routeFile, appDir, aliasMap) {
|
|
16159
|
+
let content;
|
|
16160
|
+
try {
|
|
16161
|
+
content = readFileSync9(routeFile, "utf-8");
|
|
16162
|
+
} catch {
|
|
16163
|
+
return null;
|
|
16164
|
+
}
|
|
16165
|
+
const body = extractFunctionBody(content, "generateStaticParams");
|
|
16166
|
+
if (!body)
|
|
16167
|
+
return null;
|
|
16168
|
+
const returnExpr = extractReturnExpression(body);
|
|
16169
|
+
if (!returnExpr)
|
|
16170
|
+
return null;
|
|
16171
|
+
return resolveParamsExpression(returnExpr, content, routeFile, appDir, aliasMap, new Set, 0);
|
|
16172
|
+
}
|
|
16173
|
+
function extractFunctionBody(source, name) {
|
|
16174
|
+
const fnRe = new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*\\{`, "g");
|
|
16175
|
+
let m = fnRe.exec(source);
|
|
16176
|
+
if (m) {
|
|
16177
|
+
return readBalanced(source, m.index + m[0].length - 1);
|
|
16178
|
+
}
|
|
16179
|
+
const arrowRe = new RegExp(`(?:export\\s+)?const\\s+${name}\\s*=\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>\\s*\\{`, "g");
|
|
16180
|
+
m = arrowRe.exec(source);
|
|
16181
|
+
if (m) {
|
|
16182
|
+
return readBalanced(source, m.index + m[0].length - 1);
|
|
16183
|
+
}
|
|
16184
|
+
return null;
|
|
16185
|
+
}
|
|
16186
|
+
function readBalanced(source, start) {
|
|
16187
|
+
if (source[start] !== "{")
|
|
16188
|
+
return null;
|
|
16189
|
+
let depth = 0;
|
|
16190
|
+
let i = start;
|
|
16191
|
+
let inString = null;
|
|
16192
|
+
let inLineComment = false;
|
|
16193
|
+
let inBlockComment = false;
|
|
16194
|
+
for (;i < source.length; i++) {
|
|
16195
|
+
const c = source[i];
|
|
16196
|
+
const next = source[i + 1];
|
|
16197
|
+
if (inLineComment) {
|
|
16198
|
+
if (c === `
|
|
16199
|
+
`)
|
|
16200
|
+
inLineComment = false;
|
|
16201
|
+
continue;
|
|
16202
|
+
}
|
|
16203
|
+
if (inBlockComment) {
|
|
16204
|
+
if (c === "*" && next === "/") {
|
|
16205
|
+
inBlockComment = false;
|
|
16206
|
+
i++;
|
|
16207
|
+
}
|
|
16208
|
+
continue;
|
|
16209
|
+
}
|
|
16210
|
+
if (inString) {
|
|
16211
|
+
if (c === "\\") {
|
|
16212
|
+
i++;
|
|
16213
|
+
continue;
|
|
16214
|
+
}
|
|
16215
|
+
if (c === inString)
|
|
16216
|
+
inString = null;
|
|
16217
|
+
continue;
|
|
16218
|
+
}
|
|
16219
|
+
if (c === "/" && next === "/") {
|
|
16220
|
+
inLineComment = true;
|
|
16221
|
+
i++;
|
|
16222
|
+
continue;
|
|
16223
|
+
}
|
|
16224
|
+
if (c === "/" && next === "*") {
|
|
16225
|
+
inBlockComment = true;
|
|
16226
|
+
i++;
|
|
16227
|
+
continue;
|
|
16228
|
+
}
|
|
16229
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
16230
|
+
inString = c;
|
|
16231
|
+
continue;
|
|
16232
|
+
}
|
|
16233
|
+
if (c === "{")
|
|
16234
|
+
depth++;
|
|
16235
|
+
else if (c === "}") {
|
|
16236
|
+
depth--;
|
|
16237
|
+
if (depth === 0)
|
|
16238
|
+
return source.slice(start + 1, i);
|
|
16239
|
+
}
|
|
16240
|
+
}
|
|
16241
|
+
return null;
|
|
16242
|
+
}
|
|
16243
|
+
function extractReturnExpression(body) {
|
|
16244
|
+
const re = /\breturn\s+([\s\S]+?)(?:;|$)/g;
|
|
16245
|
+
let last = null;
|
|
16246
|
+
let m;
|
|
16247
|
+
while ((m = re.exec(body)) !== null)
|
|
16248
|
+
last = m;
|
|
16249
|
+
if (!last)
|
|
16250
|
+
return null;
|
|
16251
|
+
return last[1].trim();
|
|
16252
|
+
}
|
|
16253
|
+
function resolveParamsExpression(expr, fileContent, filePath, appDir, aliasMap, visited, depth) {
|
|
16254
|
+
if (depth > 4)
|
|
16255
|
+
return null;
|
|
16256
|
+
expr = expr.trim();
|
|
16257
|
+
if (expr.startsWith("[")) {
|
|
16258
|
+
const arrText = sliceTopLevelArray(expr);
|
|
16259
|
+
if (arrText) {
|
|
16260
|
+
const tail = expr.slice(arrText.length).trim();
|
|
16261
|
+
if (!tail) {
|
|
16262
|
+
const arr = parseObjectArray(arrText);
|
|
16263
|
+
if (arr)
|
|
16264
|
+
return mergeParams(arr);
|
|
16265
|
+
}
|
|
16266
|
+
if (tail.startsWith(".map")) {
|
|
16267
|
+
const stringArr = parseStringArray(arrText);
|
|
16268
|
+
if (stringArr) {
|
|
16269
|
+
const paramName = inferParamFromMap(expr) ?? "slug";
|
|
16270
|
+
return { [paramName]: new Set(stringArr) };
|
|
16271
|
+
}
|
|
16272
|
+
const objArr = parseObjectArray(arrText);
|
|
16273
|
+
if (objArr) {
|
|
16274
|
+
const projection = extractMapProjection(expr);
|
|
16275
|
+
if (!projection)
|
|
16276
|
+
return null;
|
|
16277
|
+
const out = {};
|
|
16278
|
+
for (const [param, source] of Object.entries(projection)) {
|
|
16279
|
+
const parts = source.split(".");
|
|
16280
|
+
parts.shift();
|
|
16281
|
+
const values = [];
|
|
16282
|
+
for (const obj of objArr) {
|
|
16283
|
+
let v = obj;
|
|
16284
|
+
for (const p of parts) {
|
|
16285
|
+
if (v && typeof v === "object" && p in v) {
|
|
16286
|
+
v = v[p];
|
|
16287
|
+
} else {
|
|
16288
|
+
v = undefined;
|
|
16289
|
+
break;
|
|
16290
|
+
}
|
|
16291
|
+
}
|
|
16292
|
+
if (typeof v === "string")
|
|
16293
|
+
values.push(v);
|
|
16294
|
+
}
|
|
16295
|
+
if (values.length > 0)
|
|
16296
|
+
out[param] = new Set(values);
|
|
16297
|
+
}
|
|
16298
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
16299
|
+
}
|
|
16300
|
+
}
|
|
16301
|
+
}
|
|
16302
|
+
}
|
|
16303
|
+
const objKeysM = /^Object\.(keys|entries)\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/.exec(expr);
|
|
16304
|
+
if (objKeysM) {
|
|
16305
|
+
const ident = objKeysM[2];
|
|
16306
|
+
const keys = resolveIdentifierKeys(ident, fileContent, filePath, appDir, aliasMap, visited, depth);
|
|
16307
|
+
if (!keys)
|
|
16308
|
+
return null;
|
|
16309
|
+
const paramName = inferParamFromMap(expr) ?? guessFirstParamName(expr) ?? "slug";
|
|
16310
|
+
return { [paramName]: new Set(keys) };
|
|
16311
|
+
}
|
|
16312
|
+
const identMapM = /^([A-Za-z_$][\w$]*)\s*\.\s*map\s*\(/.exec(expr);
|
|
16313
|
+
if (identMapM) {
|
|
16314
|
+
const ident = identMapM[1];
|
|
16315
|
+
const resolved = resolveIdentifierAsArray(ident, fileContent, filePath, appDir, aliasMap, visited, depth);
|
|
16316
|
+
if (!resolved)
|
|
16317
|
+
return null;
|
|
16318
|
+
if (resolved.kind === "strings") {
|
|
16319
|
+
const paramName = inferParamFromMap(expr) ?? "slug";
|
|
16320
|
+
return { [paramName]: new Set(resolved.values) };
|
|
16321
|
+
}
|
|
16322
|
+
if (resolved.kind === "objects") {
|
|
16323
|
+
const projection = extractMapProjection(expr);
|
|
16324
|
+
if (!projection)
|
|
16325
|
+
return null;
|
|
16326
|
+
const out = {};
|
|
16327
|
+
for (const [param, source] of Object.entries(projection)) {
|
|
16328
|
+
const values = [];
|
|
16329
|
+
for (const obj of resolved.values) {
|
|
16330
|
+
const parts = source.split(".");
|
|
16331
|
+
parts.shift();
|
|
16332
|
+
let v = obj;
|
|
16333
|
+
for (const p of parts) {
|
|
16334
|
+
if (v && typeof v === "object" && p in v) {
|
|
16335
|
+
v = v[p];
|
|
16336
|
+
} else {
|
|
16337
|
+
v = undefined;
|
|
16338
|
+
break;
|
|
16339
|
+
}
|
|
16340
|
+
}
|
|
16341
|
+
if (typeof v === "string")
|
|
16342
|
+
values.push(v);
|
|
16343
|
+
}
|
|
16344
|
+
if (values.length > 0)
|
|
16345
|
+
out[param] = new Set(values);
|
|
16346
|
+
}
|
|
16347
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
16348
|
+
}
|
|
16349
|
+
if (resolved.kind === "objectKeys") {
|
|
16350
|
+
const paramName = inferParamFromMap(expr) ?? "slug";
|
|
16351
|
+
return { [paramName]: new Set(resolved.values) };
|
|
16352
|
+
}
|
|
16353
|
+
}
|
|
16354
|
+
return null;
|
|
16355
|
+
}
|
|
16356
|
+
function extractMapProjection(expr) {
|
|
16357
|
+
const bodyMatch = /\.map\s*\(\s*(?:\(([^)]*)\)|([A-Za-z_$][\w$]*))\s*=>\s*\(?\s*\{([^}]+)\}/.exec(expr);
|
|
16358
|
+
if (!bodyMatch)
|
|
16359
|
+
return null;
|
|
16360
|
+
const iter = (bodyMatch[1] ?? bodyMatch[2] ?? "").trim().split(",")[0].trim() || "item";
|
|
16361
|
+
const objBody = bodyMatch[3];
|
|
16362
|
+
const out = {};
|
|
16363
|
+
const propRe = /([A-Za-z_$][\w$]*)\s*(?::\s*([A-Za-z_$][\w$.]*))?/g;
|
|
16364
|
+
let m;
|
|
16365
|
+
while ((m = propRe.exec(objBody)) !== null) {
|
|
16366
|
+
const key = m[1];
|
|
16367
|
+
const value = m[2] ?? key;
|
|
16368
|
+
out[key] = value.startsWith(iter + ".") || value === iter ? value : `${iter}.${value}`;
|
|
16369
|
+
}
|
|
16370
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
16371
|
+
}
|
|
16372
|
+
function inferParamFromMap(expr) {
|
|
16373
|
+
const m = /=>\s*\(?\s*\{\s*([A-Za-z_$][\w$]*)\s*[:}]/.exec(expr);
|
|
16374
|
+
return m ? m[1] : null;
|
|
16375
|
+
}
|
|
16376
|
+
function guessFirstParamName(expr) {
|
|
16377
|
+
const m = /\{\s*([A-Za-z_$][\w$]*)\s*\}/.exec(expr);
|
|
16378
|
+
return m ? m[1] : null;
|
|
16379
|
+
}
|
|
16380
|
+
function mergeParams(items) {
|
|
16381
|
+
const out = {};
|
|
16382
|
+
for (const item of items) {
|
|
16383
|
+
for (const [k, v] of Object.entries(item)) {
|
|
16384
|
+
if (typeof v !== "string")
|
|
16385
|
+
continue;
|
|
16386
|
+
if (!out[k])
|
|
16387
|
+
out[k] = new Set;
|
|
16388
|
+
out[k].add(v);
|
|
16389
|
+
}
|
|
16390
|
+
}
|
|
16391
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
16392
|
+
}
|
|
16393
|
+
function parseObjectArray(expr) {
|
|
16394
|
+
const arrText = sliceTopLevelArray(expr);
|
|
16395
|
+
if (!arrText)
|
|
16396
|
+
return null;
|
|
16397
|
+
const inner = arrText.slice(1, -1).trim();
|
|
16398
|
+
if (!inner)
|
|
16399
|
+
return [];
|
|
16400
|
+
const items = [];
|
|
16401
|
+
let depth = 0;
|
|
16402
|
+
let buf = "";
|
|
16403
|
+
const chunks = [];
|
|
16404
|
+
let inString = null;
|
|
16405
|
+
for (let i = 0;i < inner.length; i++) {
|
|
16406
|
+
const c = inner[i];
|
|
16407
|
+
if (inString) {
|
|
16408
|
+
if (c === "\\") {
|
|
16409
|
+
buf += c + inner[++i];
|
|
16410
|
+
continue;
|
|
16411
|
+
}
|
|
16412
|
+
if (c === inString)
|
|
16413
|
+
inString = null;
|
|
16414
|
+
buf += c;
|
|
16415
|
+
continue;
|
|
16416
|
+
}
|
|
16417
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
16418
|
+
inString = c;
|
|
16419
|
+
buf += c;
|
|
16420
|
+
continue;
|
|
16421
|
+
}
|
|
16422
|
+
if (c === "{" || c === "[" || c === "(")
|
|
16423
|
+
depth++;
|
|
16424
|
+
else if (c === "}" || c === "]" || c === ")")
|
|
16425
|
+
depth--;
|
|
16426
|
+
if (c === "," && depth === 0) {
|
|
16427
|
+
chunks.push(buf);
|
|
16428
|
+
buf = "";
|
|
16429
|
+
continue;
|
|
16430
|
+
}
|
|
16431
|
+
buf += c;
|
|
16432
|
+
}
|
|
16433
|
+
if (buf.trim())
|
|
16434
|
+
chunks.push(buf);
|
|
16435
|
+
for (const chunk of chunks) {
|
|
16436
|
+
const obj = parseSimpleObject(chunk.trim());
|
|
16437
|
+
if (!obj)
|
|
16438
|
+
return null;
|
|
16439
|
+
items.push(obj);
|
|
16440
|
+
}
|
|
16441
|
+
return items;
|
|
16442
|
+
}
|
|
16443
|
+
function parseStringArray(arrText) {
|
|
16444
|
+
if (!arrText.startsWith("[") || !arrText.endsWith("]"))
|
|
16445
|
+
return null;
|
|
16446
|
+
const inner = arrText.slice(1, -1).trim();
|
|
16447
|
+
if (!inner)
|
|
16448
|
+
return [];
|
|
16449
|
+
if (/[{[]/.test(inner))
|
|
16450
|
+
return null;
|
|
16451
|
+
const out = [];
|
|
16452
|
+
const re = /["'`]([^"'`]*)["'`]/g;
|
|
16453
|
+
let m;
|
|
16454
|
+
let count = 0;
|
|
16455
|
+
while ((m = re.exec(inner)) !== null) {
|
|
16456
|
+
out.push(m[1]);
|
|
16457
|
+
count++;
|
|
16458
|
+
}
|
|
16459
|
+
const chunks = inner.split(",").filter((s) => s.trim()).length;
|
|
16460
|
+
if (count !== chunks)
|
|
16461
|
+
return null;
|
|
16462
|
+
return out;
|
|
16463
|
+
}
|
|
16464
|
+
function sliceTopLevelArray(expr) {
|
|
16465
|
+
if (expr[0] !== "[")
|
|
16466
|
+
return null;
|
|
16467
|
+
let depth = 0;
|
|
16468
|
+
let inString = null;
|
|
16469
|
+
for (let i = 0;i < expr.length; i++) {
|
|
16470
|
+
const c = expr[i];
|
|
16471
|
+
if (inString) {
|
|
16472
|
+
if (c === "\\") {
|
|
16473
|
+
i++;
|
|
16474
|
+
continue;
|
|
16475
|
+
}
|
|
16476
|
+
if (c === inString)
|
|
16477
|
+
inString = null;
|
|
16478
|
+
continue;
|
|
16479
|
+
}
|
|
16480
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
16481
|
+
inString = c;
|
|
16482
|
+
continue;
|
|
16483
|
+
}
|
|
16484
|
+
if (c === "[")
|
|
16485
|
+
depth++;
|
|
16486
|
+
else if (c === "]") {
|
|
16487
|
+
depth--;
|
|
16488
|
+
if (depth === 0)
|
|
16489
|
+
return expr.slice(0, i + 1);
|
|
16490
|
+
}
|
|
16491
|
+
}
|
|
16492
|
+
return null;
|
|
16493
|
+
}
|
|
16494
|
+
function parseSimpleObject(text) {
|
|
16495
|
+
if (!text.startsWith("{") || !text.endsWith("}"))
|
|
16496
|
+
return null;
|
|
16497
|
+
const body = text.slice(1, -1);
|
|
16498
|
+
const out = {};
|
|
16499
|
+
const re = /([A-Za-z_$][\w$]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/g;
|
|
16500
|
+
let m;
|
|
16501
|
+
let count = 0;
|
|
16502
|
+
while ((m = re.exec(body)) !== null) {
|
|
16503
|
+
out[m[1]] = m[2] ?? m[3] ?? m[4] ?? "";
|
|
16504
|
+
count++;
|
|
16505
|
+
}
|
|
16506
|
+
return count > 0 ? out : null;
|
|
16507
|
+
}
|
|
16508
|
+
function resolveIdentifierKeys(ident, fileContent, filePath, appDir, aliasMap, visited, depth) {
|
|
16509
|
+
const r = resolveIdentifier(ident, fileContent, filePath, appDir, aliasMap, visited, depth);
|
|
16510
|
+
if (!r)
|
|
16511
|
+
return null;
|
|
16512
|
+
if (r.kind === "objectKeys")
|
|
16513
|
+
return r.values;
|
|
16514
|
+
if (r.kind === "objects") {
|
|
16515
|
+
return null;
|
|
16516
|
+
}
|
|
16517
|
+
if (r.kind === "strings")
|
|
16518
|
+
return r.values;
|
|
16519
|
+
return null;
|
|
16520
|
+
}
|
|
16521
|
+
function resolveIdentifierAsArray(ident, fileContent, filePath, appDir, aliasMap, visited, depth) {
|
|
16522
|
+
return resolveIdentifier(ident, fileContent, filePath, appDir, aliasMap, visited, depth);
|
|
16523
|
+
}
|
|
16524
|
+
function resolveIdentifier(ident, fileContent, filePath, appDir, aliasMap, visited, depth) {
|
|
16525
|
+
if (depth > 5)
|
|
16526
|
+
return null;
|
|
16527
|
+
const visitKey = `${filePath}::${ident}`;
|
|
16528
|
+
if (visited.has(visitKey))
|
|
16529
|
+
return null;
|
|
16530
|
+
visited.add(visitKey);
|
|
16531
|
+
const localValue = findLocalConst(ident, fileContent);
|
|
16532
|
+
if (localValue) {
|
|
16533
|
+
const parsed = parseLiteralValue(localValue);
|
|
16534
|
+
if (parsed)
|
|
16535
|
+
return parsed;
|
|
16536
|
+
}
|
|
16537
|
+
const importInfo = findImport(ident, fileContent);
|
|
16538
|
+
if (!importInfo)
|
|
16539
|
+
return null;
|
|
16540
|
+
const resolvedPath = resolveModulePath(importInfo.path, filePath, appDir, aliasMap);
|
|
16541
|
+
if (!resolvedPath)
|
|
16542
|
+
return null;
|
|
16543
|
+
if (resolvedPath.endsWith(JSON_EXT)) {
|
|
16544
|
+
try {
|
|
16545
|
+
const data = JSON.parse(readFileSync9(resolvedPath, "utf-8"));
|
|
16546
|
+
return literalToResolution(data);
|
|
16547
|
+
} catch {
|
|
16548
|
+
return null;
|
|
16549
|
+
}
|
|
16550
|
+
}
|
|
16551
|
+
let nextContent;
|
|
16552
|
+
try {
|
|
16553
|
+
nextContent = readFileSync9(resolvedPath, "utf-8");
|
|
16554
|
+
} catch {
|
|
16555
|
+
return null;
|
|
16556
|
+
}
|
|
16557
|
+
const nextIdent = importInfo.kind === "default" ? findDefaultExportIdentifier(nextContent) ?? ident : importInfo.imported;
|
|
16558
|
+
return resolveIdentifier(nextIdent, nextContent, resolvedPath, appDir, aliasMap, visited, depth + 1);
|
|
16559
|
+
}
|
|
16560
|
+
function findImport(ident, source) {
|
|
16561
|
+
const defRe = new RegExp(`import\\s+${ident}\\s+from\\s+['"]([^'"]+)['"]`, "g");
|
|
16562
|
+
let m;
|
|
16563
|
+
if ((m = defRe.exec(source)) !== null) {
|
|
16564
|
+
return { kind: "default", imported: ident, path: m[1] };
|
|
16565
|
+
}
|
|
16566
|
+
const namedRe = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
16567
|
+
while ((m = namedRe.exec(source)) !== null) {
|
|
16568
|
+
const specifiers = m[1].split(",").map((s) => s.trim());
|
|
16569
|
+
for (const spec of specifiers) {
|
|
16570
|
+
const aliasMatch = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(spec);
|
|
16571
|
+
if (aliasMatch) {
|
|
16572
|
+
if (aliasMatch[2] === ident) {
|
|
16573
|
+
return { kind: "named", imported: aliasMatch[1], path: m[2] };
|
|
16574
|
+
}
|
|
16575
|
+
} else if (spec === ident) {
|
|
16576
|
+
return { kind: "named", imported: ident, path: m[2] };
|
|
16577
|
+
}
|
|
16578
|
+
}
|
|
16579
|
+
}
|
|
16580
|
+
return null;
|
|
16581
|
+
}
|
|
16582
|
+
function resolveModulePath(spec, fromFile, appDir, aliasMap) {
|
|
16583
|
+
let candidates = [];
|
|
16584
|
+
if (spec.startsWith(".")) {
|
|
16585
|
+
candidates.push(resolve3(dirname5(fromFile), spec));
|
|
16586
|
+
} else if (aliasMap.size > 0) {
|
|
16587
|
+
for (const [prefix, targets] of aliasMap.entries()) {
|
|
16588
|
+
if (spec === prefix.replace(/\/$/, "") || spec.startsWith(prefix)) {
|
|
16589
|
+
const sub = spec.slice(prefix.length);
|
|
16590
|
+
for (const t of targets) {
|
|
16591
|
+
candidates.push(sub ? join7(t, sub) : t);
|
|
16592
|
+
}
|
|
16593
|
+
}
|
|
16594
|
+
}
|
|
16595
|
+
} else {
|
|
16596
|
+
candidates.push(join7(appDir, spec));
|
|
16597
|
+
}
|
|
16598
|
+
for (const cand of candidates) {
|
|
16599
|
+
if (existsSync7(cand)) {
|
|
16600
|
+
try {
|
|
16601
|
+
if (statSync2(cand).isFile())
|
|
16602
|
+
return cand;
|
|
16603
|
+
} catch {}
|
|
16604
|
+
}
|
|
16605
|
+
for (const ext2 of [...TS_EXTS, JSON_EXT]) {
|
|
16606
|
+
if (existsSync7(cand + ext2))
|
|
16607
|
+
return cand + ext2;
|
|
16608
|
+
}
|
|
16609
|
+
for (const ext2 of TS_EXTS) {
|
|
16610
|
+
const idx = join7(cand, "index" + ext2);
|
|
16611
|
+
if (existsSync7(idx))
|
|
16612
|
+
return idx;
|
|
16613
|
+
}
|
|
16614
|
+
}
|
|
16615
|
+
return null;
|
|
16616
|
+
}
|
|
16617
|
+
function findLocalConst(ident, source) {
|
|
16618
|
+
const re = new RegExp(`(?:export\\s+)?const\\s+${ident}\\s*(?::[^=]+)?=\\s*`, "g");
|
|
16619
|
+
const m = re.exec(source);
|
|
16620
|
+
if (!m)
|
|
16621
|
+
return null;
|
|
16622
|
+
const start = m.index + m[0].length;
|
|
16623
|
+
return readExpression(source, start);
|
|
16624
|
+
}
|
|
16625
|
+
function readExpression(source, start) {
|
|
16626
|
+
let depth = 0;
|
|
16627
|
+
let inString = null;
|
|
16628
|
+
let i = start;
|
|
16629
|
+
for (;i < source.length; i++) {
|
|
16630
|
+
const c = source[i];
|
|
16631
|
+
if (inString) {
|
|
16632
|
+
if (c === "\\") {
|
|
16633
|
+
i++;
|
|
16634
|
+
continue;
|
|
16635
|
+
}
|
|
16636
|
+
if (c === inString)
|
|
16637
|
+
inString = null;
|
|
16638
|
+
continue;
|
|
16639
|
+
}
|
|
16640
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
16641
|
+
inString = c;
|
|
16642
|
+
continue;
|
|
16643
|
+
}
|
|
16644
|
+
if (c === "{" || c === "[" || c === "(")
|
|
16645
|
+
depth++;
|
|
16646
|
+
else if (c === "}" || c === "]" || c === ")")
|
|
16647
|
+
depth--;
|
|
16648
|
+
if (depth === 0 && (c === ";" || c === `
|
|
16649
|
+
`))
|
|
16650
|
+
break;
|
|
16651
|
+
}
|
|
16652
|
+
return source.slice(start, i).trim().replace(/[;]+$/, "").trim();
|
|
16653
|
+
}
|
|
16654
|
+
function findDefaultExportIdentifier(source) {
|
|
16655
|
+
const m = /export\s+default\s+([A-Za-z_$][\w$]*)\s*;?/.exec(source);
|
|
16656
|
+
return m ? m[1] : null;
|
|
16657
|
+
}
|
|
16658
|
+
function parseLiteralValue(expr) {
|
|
16659
|
+
expr = expr.trim();
|
|
16660
|
+
if (expr.startsWith("{")) {
|
|
16661
|
+
const keys = extractTopLevelKeys(expr);
|
|
16662
|
+
if (keys.length > 0)
|
|
16663
|
+
return { kind: "objectKeys", values: keys };
|
|
16664
|
+
return null;
|
|
16665
|
+
}
|
|
16666
|
+
if (expr.startsWith("[")) {
|
|
16667
|
+
const arrText = sliceTopLevelArray(expr);
|
|
16668
|
+
if (!arrText)
|
|
16669
|
+
return null;
|
|
16670
|
+
const stringRe = /["'`]([^"'`]*)["'`]/g;
|
|
16671
|
+
const inner = arrText.slice(1, -1);
|
|
16672
|
+
if (/^\s*\{/.test(inner)) {
|
|
16673
|
+
const objs = parseObjectArray(arrText);
|
|
16674
|
+
if (objs)
|
|
16675
|
+
return { kind: "objects", values: objs };
|
|
16676
|
+
return null;
|
|
16677
|
+
}
|
|
16678
|
+
const values = [];
|
|
16679
|
+
let m;
|
|
16680
|
+
while ((m = stringRe.exec(inner)) !== null)
|
|
16681
|
+
values.push(m[1]);
|
|
16682
|
+
if (values.length > 0)
|
|
16683
|
+
return { kind: "strings", values };
|
|
16684
|
+
}
|
|
16685
|
+
return null;
|
|
16686
|
+
}
|
|
16687
|
+
function extractTopLevelKeys(expr) {
|
|
16688
|
+
const inner = expr.slice(1, expr.length - 1);
|
|
16689
|
+
const keys = [];
|
|
16690
|
+
let depth = 0;
|
|
16691
|
+
let i = 0;
|
|
16692
|
+
let inString = null;
|
|
16693
|
+
let atKeyPos = true;
|
|
16694
|
+
while (i < inner.length) {
|
|
16695
|
+
const c = inner[i];
|
|
16696
|
+
if (inString) {
|
|
16697
|
+
if (c === "\\") {
|
|
16698
|
+
i += 2;
|
|
16699
|
+
continue;
|
|
16700
|
+
}
|
|
16701
|
+
if (c === inString)
|
|
16702
|
+
inString = null;
|
|
16703
|
+
i++;
|
|
16704
|
+
continue;
|
|
16705
|
+
}
|
|
16706
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
16707
|
+
if (atKeyPos && depth === 0) {
|
|
16708
|
+
const quote = c;
|
|
16709
|
+
const end = inner.indexOf(quote, i + 1);
|
|
16710
|
+
if (end === -1)
|
|
16711
|
+
break;
|
|
16712
|
+
keys.push(inner.slice(i + 1, end));
|
|
16713
|
+
i = end + 1;
|
|
16714
|
+
while (i < inner.length && /\s/.test(inner[i]))
|
|
16715
|
+
i++;
|
|
16716
|
+
if (inner[i] === ":") {
|
|
16717
|
+
i++;
|
|
16718
|
+
atKeyPos = false;
|
|
16719
|
+
}
|
|
16720
|
+
continue;
|
|
16721
|
+
}
|
|
16722
|
+
inString = c;
|
|
16723
|
+
i++;
|
|
16724
|
+
continue;
|
|
16725
|
+
}
|
|
16726
|
+
if (c === "{" || c === "[" || c === "(") {
|
|
16727
|
+
depth++;
|
|
16728
|
+
i++;
|
|
16729
|
+
continue;
|
|
16730
|
+
}
|
|
16731
|
+
if (c === "}" || c === "]" || c === ")") {
|
|
16732
|
+
depth--;
|
|
16733
|
+
i++;
|
|
16734
|
+
continue;
|
|
16735
|
+
}
|
|
16736
|
+
if (depth === 0 && c === ",") {
|
|
16737
|
+
atKeyPos = true;
|
|
16738
|
+
i++;
|
|
16739
|
+
continue;
|
|
16740
|
+
}
|
|
16741
|
+
if (depth === 0 && c === ":") {
|
|
16742
|
+
atKeyPos = false;
|
|
16743
|
+
i++;
|
|
16744
|
+
continue;
|
|
16745
|
+
}
|
|
16746
|
+
if (atKeyPos && depth === 0 && /[A-Za-z_$]/.test(c)) {
|
|
16747
|
+
let j = i;
|
|
16748
|
+
while (j < inner.length && /[\w$]/.test(inner[j]))
|
|
16749
|
+
j++;
|
|
16750
|
+
const key = inner.slice(i, j);
|
|
16751
|
+
let k = j;
|
|
16752
|
+
while (k < inner.length && /\s/.test(inner[k]))
|
|
16753
|
+
k++;
|
|
16754
|
+
if (inner[k] === ":") {
|
|
16755
|
+
keys.push(key);
|
|
16756
|
+
i = k + 1;
|
|
16757
|
+
atKeyPos = false;
|
|
16758
|
+
continue;
|
|
16759
|
+
}
|
|
16760
|
+
i = j;
|
|
16761
|
+
continue;
|
|
16762
|
+
}
|
|
16763
|
+
i++;
|
|
16764
|
+
}
|
|
16765
|
+
return keys;
|
|
16766
|
+
}
|
|
16767
|
+
function literalToResolution(data) {
|
|
16768
|
+
if (Array.isArray(data)) {
|
|
16769
|
+
if (data.every((v) => typeof v === "string")) {
|
|
16770
|
+
return { kind: "strings", values: data };
|
|
16771
|
+
}
|
|
16772
|
+
if (data.every((v) => v && typeof v === "object")) {
|
|
16773
|
+
return { kind: "objects", values: data };
|
|
16774
|
+
}
|
|
16775
|
+
return null;
|
|
16776
|
+
}
|
|
16777
|
+
if (data && typeof data === "object") {
|
|
16778
|
+
return { kind: "objectKeys", values: Object.keys(data) };
|
|
16779
|
+
}
|
|
16780
|
+
return null;
|
|
16781
|
+
}
|
|
16132
16782
|
|
|
16133
16783
|
// src/scanner.ts
|
|
16134
16784
|
function extractRoutePath(filePath) {
|
|
@@ -16634,7 +17284,7 @@ async function scan(config) {
|
|
|
16634
17284
|
// src/config.ts
|
|
16635
17285
|
var import_fast_glob11 = __toESM(require_out4(), 1);
|
|
16636
17286
|
import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
|
|
16637
|
-
import { join as join9, resolve as
|
|
17287
|
+
import { join as join9, resolve as resolve4, relative as relative4, dirname as dirname6 } from "node:path";
|
|
16638
17288
|
var DEFAULT_CONFIG = {
|
|
16639
17289
|
dir: "./",
|
|
16640
17290
|
ignore: {
|
|
@@ -16672,7 +17322,7 @@ function loadConfig(options) {
|
|
|
16672
17322
|
absolute: true
|
|
16673
17323
|
});
|
|
16674
17324
|
if (options.config && existsSync9(options.config)) {
|
|
16675
|
-
const absConfig =
|
|
17325
|
+
const absConfig = resolve4(cwd, options.config);
|
|
16676
17326
|
if (!configFiles.includes(absConfig)) {
|
|
16677
17327
|
configFiles.push(absConfig);
|
|
16678
17328
|
}
|
|
@@ -16695,7 +17345,7 @@ function loadConfig(options) {
|
|
|
16695
17345
|
try {
|
|
16696
17346
|
const content = readFileSync11(configPath, "utf-8");
|
|
16697
17347
|
const config = JSON.parse(content);
|
|
16698
|
-
const configDir =
|
|
17348
|
+
const configDir = dirname6(configPath);
|
|
16699
17349
|
const relDir = relative4(cwd, configDir);
|
|
16700
17350
|
const prefixPattern = (p) => {
|
|
16701
17351
|
if (p.startsWith("**/") || p.startsWith("/") || !relDir)
|
|
@@ -16810,7 +17460,7 @@ program2.action(async (options) => {
|
|
|
16810
17460
|
let isMonorepo = existsSync11(appsDir) && lstatSync(appsDir).isDirectory();
|
|
16811
17461
|
if (!isMonorepo) {
|
|
16812
17462
|
let current = absoluteDir;
|
|
16813
|
-
while (current !==
|
|
17463
|
+
while (current !== dirname7(current)) {
|
|
16814
17464
|
const potentialApps = join11(current, "apps");
|
|
16815
17465
|
if (existsSync11(potentialApps) && lstatSync(potentialApps).isDirectory()) {
|
|
16816
17466
|
monorepoRoot = current;
|
|
@@ -16818,7 +17468,7 @@ program2.action(async (options) => {
|
|
|
16818
17468
|
isMonorepo = true;
|
|
16819
17469
|
break;
|
|
16820
17470
|
}
|
|
16821
|
-
current =
|
|
17471
|
+
current = dirname7(current);
|
|
16822
17472
|
}
|
|
16823
17473
|
}
|
|
16824
17474
|
if (isMonorepo && monorepoRoot !== absoluteDir) {
|
|
@@ -16919,6 +17569,10 @@ program2.action(async (options) => {
|
|
|
16919
17569
|
if (options.json) {
|
|
16920
17570
|
console.log(JSON.stringify(result, null, 2));
|
|
16921
17571
|
} else if (options.fix) {
|
|
17572
|
+
if (isScanAll && !hasUnusedItems(result)) {
|
|
17573
|
+
console.log(source_default.green(` ✅ Nothing to fix in ${appLabel}`));
|
|
17574
|
+
continue;
|
|
17575
|
+
}
|
|
16922
17576
|
const fixResult = await handleFixes(result, currentConfig, options, isMonorepo);
|
|
16923
17577
|
if (fixResult === "back") {
|
|
16924
17578
|
requestedBack = true;
|
|
@@ -17161,7 +17815,7 @@ Analyzing cascading impact...`));
|
|
|
17161
17815
|
const routeFiles = new Set;
|
|
17162
17816
|
const rootDir = config.appSpecificScan ? config.appSpecificScan.rootDir : config.dir;
|
|
17163
17817
|
for (const r of [...unusedRoutes, ...partiallyRoutes]) {
|
|
17164
|
-
routeFiles.add(
|
|
17818
|
+
routeFiles.add(resolve5(rootDir, r.filePath));
|
|
17165
17819
|
}
|
|
17166
17820
|
const title = `Unused API Routes (${totalRoutesIssues} items in ${routeFiles.size} files)`;
|
|
17167
17821
|
choices.push({ title, value: "routes" });
|
|
@@ -17458,7 +18112,7 @@ Analyzing cascading impact...`));
|
|
|
17458
18112
|
const fullPath = resolveFilePath(filePath, config);
|
|
17459
18113
|
const route = fileRoutes[0];
|
|
17460
18114
|
if (route.type === "nextjs" && (filePath.includes("app/api") || filePath.includes("pages/api"))) {
|
|
17461
|
-
filesToUnlink.add(
|
|
18115
|
+
filesToUnlink.add(dirname7(fullPath));
|
|
17462
18116
|
} else if (route.type === "nestjs" && (result.unusedFiles?.files.some((f) => f.path === filePath) || filePath.includes("api/"))) {
|
|
17463
18117
|
filesToUnlink.add(fullPath);
|
|
17464
18118
|
} else {
|
|
@@ -17512,7 +18166,7 @@ Analyzing cascading impact...`));
|
|
|
17512
18166
|
}
|
|
17513
18167
|
const actuallyFixed = new Set;
|
|
17514
18168
|
for (const [absPath, removals] of removalsByFile) {
|
|
17515
|
-
if (actuallyDeleted.has(absPath) || actuallyDeleted.has(
|
|
18169
|
+
if (actuallyDeleted.has(absPath) || actuallyDeleted.has(dirname7(absPath)))
|
|
17516
18170
|
continue;
|
|
17517
18171
|
if (!existsSync11(absPath))
|
|
17518
18172
|
continue;
|
|
@@ -17527,7 +18181,7 @@ Analyzing cascading impact...`));
|
|
|
17527
18181
|
}
|
|
17528
18182
|
result.routes = result.routes.filter((r) => {
|
|
17529
18183
|
const fullPath = resolveFilePath(r.filePath, config);
|
|
17530
|
-
if (actuallyDeleted.has(fullPath) || actuallyDeleted.has(
|
|
18184
|
+
if (actuallyDeleted.has(fullPath) || actuallyDeleted.has(dirname7(fullPath)))
|
|
17531
18185
|
return false;
|
|
17532
18186
|
if (actuallyFixed.has(fullPath)) {
|
|
17533
18187
|
r.unusedMethods = [];
|
|
@@ -17999,11 +18653,11 @@ function printTable(summary) {
|
|
|
17999
18653
|
}
|
|
18000
18654
|
function findGitRoot(startDir) {
|
|
18001
18655
|
let currentDir = startDir;
|
|
18002
|
-
while (currentDir !==
|
|
18656
|
+
while (currentDir !== dirname7(currentDir)) {
|
|
18003
18657
|
if (existsSync11(join11(currentDir, ".git"))) {
|
|
18004
18658
|
return true;
|
|
18005
18659
|
}
|
|
18006
|
-
currentDir =
|
|
18660
|
+
currentDir = dirname7(currentDir);
|
|
18007
18661
|
}
|
|
18008
18662
|
return false;
|
|
18009
18663
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pruny",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.44.0",
|
|
4
4
|
"description": "Find and remove unused Next.js API routes & Nest.js Controllers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
|
-
"url": "https://github.com/
|
|
37
|
+
"url": "https://github.com/Navibyte-Innovations-Pvt-Ltd/pruny"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"chalk": "^5.3.0",
|