pruny 1.43.9 → 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.
Files changed (2) hide show
  1. package/dist/index.js +691 -31
  2. 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 dirname6, join as join11, relative as relative5, resolve as resolve4 } from "node:path";
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,16 +15886,16 @@ 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
- /<Link\s+[^>]*href\s*=\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15893
- /router\.(push|replace)\s*\(\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15894
- /(?:redirect|permanentRedirect)\s*\(\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15895
- /href\s*:\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15896
- /<a\s+[^>]*href\s*=\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15897
- /revalidatePath\s*\(\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15898
- /pathname\s*===?\s*['"`](\/[^'"`\s{}$]+)['"`]/g
15892
+ /<Link\s+[^>]*href\s*=\s*(?:\{\s*)?['"`](\/[^'"`\s]+)['"`](?:\s*\})?/g,
15893
+ /router\.(push|replace)\s*\(\s*['"`](\/[^'"`\s]+)['"`]/g,
15894
+ /(?:redirect|permanentRedirect)\s*\(\s*['"`](\/[^'"`\s]+)['"`]/g,
15895
+ /href\s*:\s*['"`](\/[^'"`\s]+)['"`]/g,
15896
+ /<a\s+[^>]*href\s*=\s*(?:\{\s*)?['"`](\/[^'"`\s]+)['"`](?:\s*\})?/g,
15897
+ /revalidatePath\s*\(\s*['"`](\/[^'"`\s]+)['"`]/g,
15898
+ /pathname\s*===?\s*['"`](\/[^'"`\s]+)['"`]/g
15899
15899
  ];
15900
15900
  function extractPath(match2) {
15901
15901
  if (match2[2] && match2[2].startsWith("/"))
@@ -15904,6 +15904,11 @@ function extractPath(match2) {
15904
15904
  return match2[1];
15905
15905
  return null;
15906
15906
  }
15907
+ function normalizePath(raw) {
15908
+ let out = raw.replace(/\$\{[^}]*\}/g, "[id]");
15909
+ out = out.replace(/[{}]/g, "");
15910
+ return out;
15911
+ }
15907
15912
  function shouldSkipPath(path2) {
15908
15913
  if (/^https?:\/\//.test(path2))
15909
15914
  return true;
@@ -15939,15 +15944,15 @@ function filePathToRoute(filePath) {
15939
15944
  });
15940
15945
  return "/" + segments.join("/");
15941
15946
  }
15942
- function matchesRoute(refPath, routes, routeSegments) {
15947
+ function matchesRoute(refPath, routes, routes_) {
15943
15948
  const cleaned = cleanPath(refPath);
15944
15949
  if (routes.has(cleaned))
15945
15950
  return true;
15946
15951
  const refSegments = cleaned.split("/").filter(Boolean);
15947
- for (const routeSeg of routeSegments) {
15948
- if (matchSegments(refSegments, routeSeg))
15952
+ for (const route of routes_) {
15953
+ if (matchSegments(refSegments, route.segments, route.params))
15949
15954
  return true;
15950
- if (matchesDynamicSuffix(refSegments, routeSeg))
15955
+ if (matchesDynamicSuffix(refSegments, route.segments))
15951
15956
  return true;
15952
15957
  }
15953
15958
  return false;
@@ -15965,14 +15970,29 @@ function matchesDynamicSuffix(refSegments, routeSegments) {
15965
15970
  return false;
15966
15971
  return matchSegments(refSegments, tail);
15967
15972
  }
15968
- function matchSegments(refSegments, routeSegments) {
15973
+ function matchSegments(refSegments, routeSegments, params) {
15969
15974
  let ri = 0;
15970
15975
  let si = 0;
15971
15976
  while (ri < refSegments.length && si < routeSegments.length) {
15972
15977
  const routeSeg = routeSegments[si];
15973
15978
  if (/^\[\[?\.\.\./.test(routeSeg))
15974
15979
  return true;
15975
- if (/^\[.+\]$/.test(routeSeg)) {
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
+ }
15976
15996
  ri++;
15977
15997
  si++;
15978
15998
  continue;
@@ -15997,7 +16017,7 @@ function isGitignoredPublicFile(appDir, linkPath) {
15997
16017
  continue;
15998
16018
  try {
15999
16019
  const patterns = readFileSync9(gitignorePath, "utf-8").split(`
16000
- `).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
16020
+ `).map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("!"));
16001
16021
  for (const pattern of patterns) {
16002
16022
  if (minimatch(publicRelPath, pattern, { dot: true }) || minimatch(linkPath.slice(1), pattern, { dot: true }) || minimatch(`**/public${linkPath}`, pattern, { dot: true })) {
16003
16023
  return true;
@@ -16025,12 +16045,21 @@ async function scanBrokenLinks(config) {
16025
16045
  const knownRoutes = new Set;
16026
16046
  const routeSegmentsList = [];
16027
16047
  knownRoutes.add("/");
16048
+ const aliasMap = parseTsConfigPaths(appDir);
16028
16049
  for (const file of pageFiles) {
16029
16050
  const route = filePathToRoute(file);
16030
16051
  knownRoutes.add(route);
16031
16052
  const segments = route.split("/").filter(Boolean);
16032
16053
  if (segments.some((s) => s.startsWith("["))) {
16033
- routeSegmentsList.push(segments);
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
+ }
16034
16063
  }
16035
16064
  }
16036
16065
  if (process.env.DEBUG_PRUNY) {
@@ -16073,9 +16102,10 @@ async function scanBrokenLinks(config) {
16073
16102
  pattern.lastIndex = 0;
16074
16103
  let match2;
16075
16104
  while ((match2 = pattern.exec(content)) !== null) {
16076
- const rawPath = extractPath(match2);
16077
- if (!rawPath)
16105
+ const extracted = extractPath(match2);
16106
+ if (!extracted)
16078
16107
  continue;
16108
+ const rawPath = normalizePath(extracted);
16079
16109
  if (shouldSkipPath(rawPath))
16080
16110
  continue;
16081
16111
  const cleaned = cleanPath(rawPath);
@@ -16123,6 +16153,632 @@ async function scanBrokenLinks(config) {
16123
16153
  links
16124
16154
  };
16125
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
+ }
16126
16782
 
16127
16783
  // src/scanner.ts
16128
16784
  function extractRoutePath(filePath) {
@@ -16628,7 +17284,7 @@ async function scan(config) {
16628
17284
  // src/config.ts
16629
17285
  var import_fast_glob11 = __toESM(require_out4(), 1);
16630
17286
  import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
16631
- import { join as join9, resolve as resolve3, relative as relative4, dirname as dirname5 } from "node:path";
17287
+ import { join as join9, resolve as resolve4, relative as relative4, dirname as dirname6 } from "node:path";
16632
17288
  var DEFAULT_CONFIG = {
16633
17289
  dir: "./",
16634
17290
  ignore: {
@@ -16666,7 +17322,7 @@ function loadConfig(options) {
16666
17322
  absolute: true
16667
17323
  });
16668
17324
  if (options.config && existsSync9(options.config)) {
16669
- const absConfig = resolve3(cwd, options.config);
17325
+ const absConfig = resolve4(cwd, options.config);
16670
17326
  if (!configFiles.includes(absConfig)) {
16671
17327
  configFiles.push(absConfig);
16672
17328
  }
@@ -16689,7 +17345,7 @@ function loadConfig(options) {
16689
17345
  try {
16690
17346
  const content = readFileSync11(configPath, "utf-8");
16691
17347
  const config = JSON.parse(content);
16692
- const configDir = dirname5(configPath);
17348
+ const configDir = dirname6(configPath);
16693
17349
  const relDir = relative4(cwd, configDir);
16694
17350
  const prefixPattern = (p) => {
16695
17351
  if (p.startsWith("**/") || p.startsWith("/") || !relDir)
@@ -16804,7 +17460,7 @@ program2.action(async (options) => {
16804
17460
  let isMonorepo = existsSync11(appsDir) && lstatSync(appsDir).isDirectory();
16805
17461
  if (!isMonorepo) {
16806
17462
  let current = absoluteDir;
16807
- while (current !== dirname6(current)) {
17463
+ while (current !== dirname7(current)) {
16808
17464
  const potentialApps = join11(current, "apps");
16809
17465
  if (existsSync11(potentialApps) && lstatSync(potentialApps).isDirectory()) {
16810
17466
  monorepoRoot = current;
@@ -16812,7 +17468,7 @@ program2.action(async (options) => {
16812
17468
  isMonorepo = true;
16813
17469
  break;
16814
17470
  }
16815
- current = dirname6(current);
17471
+ current = dirname7(current);
16816
17472
  }
16817
17473
  }
16818
17474
  if (isMonorepo && monorepoRoot !== absoluteDir) {
@@ -16913,6 +17569,10 @@ program2.action(async (options) => {
16913
17569
  if (options.json) {
16914
17570
  console.log(JSON.stringify(result, null, 2));
16915
17571
  } else if (options.fix) {
17572
+ if (isScanAll && !hasUnusedItems(result)) {
17573
+ console.log(source_default.green(` ✅ Nothing to fix in ${appLabel}`));
17574
+ continue;
17575
+ }
16916
17576
  const fixResult = await handleFixes(result, currentConfig, options, isMonorepo);
16917
17577
  if (fixResult === "back") {
16918
17578
  requestedBack = true;
@@ -17155,7 +17815,7 @@ Analyzing cascading impact...`));
17155
17815
  const routeFiles = new Set;
17156
17816
  const rootDir = config.appSpecificScan ? config.appSpecificScan.rootDir : config.dir;
17157
17817
  for (const r of [...unusedRoutes, ...partiallyRoutes]) {
17158
- routeFiles.add(resolve4(rootDir, r.filePath));
17818
+ routeFiles.add(resolve5(rootDir, r.filePath));
17159
17819
  }
17160
17820
  const title = `Unused API Routes (${totalRoutesIssues} items in ${routeFiles.size} files)`;
17161
17821
  choices.push({ title, value: "routes" });
@@ -17452,7 +18112,7 @@ Analyzing cascading impact...`));
17452
18112
  const fullPath = resolveFilePath(filePath, config);
17453
18113
  const route = fileRoutes[0];
17454
18114
  if (route.type === "nextjs" && (filePath.includes("app/api") || filePath.includes("pages/api"))) {
17455
- filesToUnlink.add(dirname6(fullPath));
18115
+ filesToUnlink.add(dirname7(fullPath));
17456
18116
  } else if (route.type === "nestjs" && (result.unusedFiles?.files.some((f) => f.path === filePath) || filePath.includes("api/"))) {
17457
18117
  filesToUnlink.add(fullPath);
17458
18118
  } else {
@@ -17506,7 +18166,7 @@ Analyzing cascading impact...`));
17506
18166
  }
17507
18167
  const actuallyFixed = new Set;
17508
18168
  for (const [absPath, removals] of removalsByFile) {
17509
- if (actuallyDeleted.has(absPath) || actuallyDeleted.has(dirname6(absPath)))
18169
+ if (actuallyDeleted.has(absPath) || actuallyDeleted.has(dirname7(absPath)))
17510
18170
  continue;
17511
18171
  if (!existsSync11(absPath))
17512
18172
  continue;
@@ -17521,7 +18181,7 @@ Analyzing cascading impact...`));
17521
18181
  }
17522
18182
  result.routes = result.routes.filter((r) => {
17523
18183
  const fullPath = resolveFilePath(r.filePath, config);
17524
- if (actuallyDeleted.has(fullPath) || actuallyDeleted.has(dirname6(fullPath)))
18184
+ if (actuallyDeleted.has(fullPath) || actuallyDeleted.has(dirname7(fullPath)))
17525
18185
  return false;
17526
18186
  if (actuallyFixed.has(fullPath)) {
17527
18187
  r.unusedMethods = [];
@@ -17993,11 +18653,11 @@ function printTable(summary) {
17993
18653
  }
17994
18654
  function findGitRoot(startDir) {
17995
18655
  let currentDir = startDir;
17996
- while (currentDir !== dirname6(currentDir)) {
18656
+ while (currentDir !== dirname7(currentDir)) {
17997
18657
  if (existsSync11(join11(currentDir, ".git"))) {
17998
18658
  return true;
17999
18659
  }
18000
- currentDir = dirname6(currentDir);
18660
+ currentDir = dirname7(currentDir);
18001
18661
  }
18002
18662
  return false;
18003
18663
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.43.9",
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/webnaresh/pruny"
37
+ "url": "https://github.com/Navibyte-Innovations-Pvt-Ltd/pruny"
38
38
  },
39
39
  "dependencies": {
40
40
  "chalk": "^5.3.0",