pruny 1.43.10 → 1.44.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +709 -22
  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,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, routeSegments) {
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 routeSeg of routeSegments) {
15953
- if (matchSegments(refSegments, routeSeg))
15952
+ for (const route of routes_) {
15953
+ if (matchSegments(refSegments, route.segments, route.params))
15954
15954
  return true;
15955
- if (matchesDynamicSuffix(refSegments, routeSeg))
15955
+ if (matchesDynamicSuffix(refSegments, route.segments))
15956
15956
  return true;
15957
15957
  }
15958
15958
  return false;
@@ -15970,25 +15970,71 @@ 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
- if (/^\[.+\]$/.test(routeSeg)) {
15980
+ const dynMatch = /^\[(.+)\]$/.exec(routeSeg);
15981
+ if (dynMatch) {
15982
+ const paramName = dynMatch[1];
15983
+ const ref = refSegments[ri];
15984
+ const isPlaceholder = /^\[.+\]$/.test(ref);
15985
+ if (params && params[paramName] && !isPlaceholder) {
15986
+ const allowed = params[paramName];
15987
+ let ok = false;
15988
+ for (const v of allowed) {
15989
+ if (v.toLowerCase() === ref.toLowerCase()) {
15990
+ ok = true;
15991
+ break;
15992
+ }
15993
+ }
15994
+ if (!ok)
15995
+ return false;
15996
+ }
15981
15997
  ri++;
15982
15998
  si++;
15983
15999
  continue;
15984
16000
  }
15985
- if (refSegments[ri].toLowerCase() !== routeSeg.toLowerCase())
16001
+ const refSeg = refSegments[ri];
16002
+ if (/^\[.+\]$/.test(refSeg)) {
16003
+ ri++;
16004
+ si++;
16005
+ continue;
16006
+ }
16007
+ if (refSeg.toLowerCase() !== routeSeg.toLowerCase())
15986
16008
  return false;
15987
16009
  ri++;
15988
16010
  si++;
15989
16011
  }
15990
16012
  return ri === refSegments.length && si === routeSegments.length;
15991
16013
  }
16014
+ function isRuntimeGeneratedPublicAsset(appDir, linkPath) {
16015
+ if (/^\/(sitemap(?:[-_].+)?\.xml|robots\.txt|manifest\.(?:json|webmanifest)|favicon\.ico|sw\.js|service-worker\.js)$/.test(linkPath)) {
16016
+ return true;
16017
+ }
16018
+ const generatorConfigs = [
16019
+ "next-sitemap.config.js",
16020
+ "next-sitemap.config.mjs",
16021
+ "next-sitemap.config.cjs",
16022
+ "next-sitemap.config.ts"
16023
+ ];
16024
+ if (linkPath.startsWith("/sitemap")) {
16025
+ for (const cfg of generatorConfigs) {
16026
+ if (existsSync7(join7(appDir, cfg)))
16027
+ return true;
16028
+ }
16029
+ for (const ext2 of ["ts", "tsx", "js", "jsx"]) {
16030
+ if (existsSync7(join7(appDir, "app", `sitemap.${ext2}`)))
16031
+ return true;
16032
+ if (existsSync7(join7(appDir, "src/app", `sitemap.${ext2}`)))
16033
+ return true;
16034
+ }
16035
+ }
16036
+ return false;
16037
+ }
15992
16038
  function isGitignoredPublicFile(appDir, linkPath) {
15993
16039
  const publicRelPath = `public${linkPath}`;
15994
16040
  const dirsToCheck = [appDir];
@@ -16030,12 +16076,21 @@ async function scanBrokenLinks(config) {
16030
16076
  const knownRoutes = new Set;
16031
16077
  const routeSegmentsList = [];
16032
16078
  knownRoutes.add("/");
16079
+ const aliasMap = parseTsConfigPaths(appDir);
16033
16080
  for (const file of pageFiles) {
16034
16081
  const route = filePathToRoute(file);
16035
16082
  knownRoutes.add(route);
16036
16083
  const segments = route.split("/").filter(Boolean);
16037
16084
  if (segments.some((s) => s.startsWith("["))) {
16038
- routeSegmentsList.push(segments);
16085
+ const absFile = join7(appDir, file);
16086
+ const params = resolveStaticParams(absFile, appDir, aliasMap);
16087
+ routeSegmentsList.push({ segments, params: params ?? undefined });
16088
+ if (process.env.DEBUG_PRUNY) {
16089
+ if (params) {
16090
+ const summary = Object.entries(params).map(([k, v]) => `${k}=${v.size}`).join(", ");
16091
+ console.log(`[DEBUG] Static params for ${route}: ${summary}`);
16092
+ }
16093
+ }
16039
16094
  }
16040
16095
  }
16041
16096
  if (process.env.DEBUG_PRUNY) {
@@ -16094,6 +16149,8 @@ async function scanBrokenLinks(config) {
16094
16149
  continue;
16095
16150
  if (isGitignoredPublicFile(appDir, cleaned))
16096
16151
  continue;
16152
+ if (isRuntimeGeneratedPublicAsset(appDir, cleaned))
16153
+ continue;
16097
16154
  const ignorePatterns = [
16098
16155
  ...config.ignore.links || [],
16099
16156
  ...config.ignore.routes
@@ -16129,6 +16186,632 @@ async function scanBrokenLinks(config) {
16129
16186
  links
16130
16187
  };
16131
16188
  }
16189
+ var TS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
16190
+ var JSON_EXT = ".json";
16191
+ function resolveStaticParams(routeFile, appDir, aliasMap) {
16192
+ let content;
16193
+ try {
16194
+ content = readFileSync9(routeFile, "utf-8");
16195
+ } catch {
16196
+ return null;
16197
+ }
16198
+ const body = extractFunctionBody(content, "generateStaticParams");
16199
+ if (!body)
16200
+ return null;
16201
+ const returnExpr = extractReturnExpression(body);
16202
+ if (!returnExpr)
16203
+ return null;
16204
+ return resolveParamsExpression(returnExpr, content, routeFile, appDir, aliasMap, new Set, 0);
16205
+ }
16206
+ function extractFunctionBody(source, name) {
16207
+ const fnRe = new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*\\{`, "g");
16208
+ let m = fnRe.exec(source);
16209
+ if (m) {
16210
+ return readBalanced(source, m.index + m[0].length - 1);
16211
+ }
16212
+ const arrowRe = new RegExp(`(?:export\\s+)?const\\s+${name}\\s*=\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>\\s*\\{`, "g");
16213
+ m = arrowRe.exec(source);
16214
+ if (m) {
16215
+ return readBalanced(source, m.index + m[0].length - 1);
16216
+ }
16217
+ return null;
16218
+ }
16219
+ function readBalanced(source, start) {
16220
+ if (source[start] !== "{")
16221
+ return null;
16222
+ let depth = 0;
16223
+ let i = start;
16224
+ let inString = null;
16225
+ let inLineComment = false;
16226
+ let inBlockComment = false;
16227
+ for (;i < source.length; i++) {
16228
+ const c = source[i];
16229
+ const next = source[i + 1];
16230
+ if (inLineComment) {
16231
+ if (c === `
16232
+ `)
16233
+ inLineComment = false;
16234
+ continue;
16235
+ }
16236
+ if (inBlockComment) {
16237
+ if (c === "*" && next === "/") {
16238
+ inBlockComment = false;
16239
+ i++;
16240
+ }
16241
+ continue;
16242
+ }
16243
+ if (inString) {
16244
+ if (c === "\\") {
16245
+ i++;
16246
+ continue;
16247
+ }
16248
+ if (c === inString)
16249
+ inString = null;
16250
+ continue;
16251
+ }
16252
+ if (c === "/" && next === "/") {
16253
+ inLineComment = true;
16254
+ i++;
16255
+ continue;
16256
+ }
16257
+ if (c === "/" && next === "*") {
16258
+ inBlockComment = true;
16259
+ i++;
16260
+ continue;
16261
+ }
16262
+ if (c === '"' || c === "'" || c === "`") {
16263
+ inString = c;
16264
+ continue;
16265
+ }
16266
+ if (c === "{")
16267
+ depth++;
16268
+ else if (c === "}") {
16269
+ depth--;
16270
+ if (depth === 0)
16271
+ return source.slice(start + 1, i);
16272
+ }
16273
+ }
16274
+ return null;
16275
+ }
16276
+ function extractReturnExpression(body) {
16277
+ const re = /\breturn\s+([\s\S]+?)(?:;|$)/g;
16278
+ let last = null;
16279
+ let m;
16280
+ while ((m = re.exec(body)) !== null)
16281
+ last = m;
16282
+ if (!last)
16283
+ return null;
16284
+ return last[1].trim();
16285
+ }
16286
+ function resolveParamsExpression(expr, fileContent, filePath, appDir, aliasMap, visited, depth) {
16287
+ if (depth > 4)
16288
+ return null;
16289
+ expr = expr.trim();
16290
+ if (expr.startsWith("[")) {
16291
+ const arrText = sliceTopLevelArray(expr);
16292
+ if (arrText) {
16293
+ const tail = expr.slice(arrText.length).trim();
16294
+ if (!tail) {
16295
+ const arr = parseObjectArray(arrText);
16296
+ if (arr)
16297
+ return mergeParams(arr);
16298
+ }
16299
+ if (tail.startsWith(".map")) {
16300
+ const stringArr = parseStringArray(arrText);
16301
+ if (stringArr) {
16302
+ const paramName = inferParamFromMap(expr) ?? "slug";
16303
+ return { [paramName]: new Set(stringArr) };
16304
+ }
16305
+ const objArr = parseObjectArray(arrText);
16306
+ if (objArr) {
16307
+ const projection = extractMapProjection(expr);
16308
+ if (!projection)
16309
+ return null;
16310
+ const out = {};
16311
+ for (const [param, source] of Object.entries(projection)) {
16312
+ const parts = source.split(".");
16313
+ parts.shift();
16314
+ const values = [];
16315
+ for (const obj of objArr) {
16316
+ let v = obj;
16317
+ for (const p of parts) {
16318
+ if (v && typeof v === "object" && p in v) {
16319
+ v = v[p];
16320
+ } else {
16321
+ v = undefined;
16322
+ break;
16323
+ }
16324
+ }
16325
+ if (typeof v === "string")
16326
+ values.push(v);
16327
+ }
16328
+ if (values.length > 0)
16329
+ out[param] = new Set(values);
16330
+ }
16331
+ return Object.keys(out).length > 0 ? out : null;
16332
+ }
16333
+ }
16334
+ }
16335
+ }
16336
+ const objKeysM = /^Object\.(keys|entries)\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/.exec(expr);
16337
+ if (objKeysM) {
16338
+ const ident = objKeysM[2];
16339
+ const keys = resolveIdentifierKeys(ident, fileContent, filePath, appDir, aliasMap, visited, depth);
16340
+ if (!keys)
16341
+ return null;
16342
+ const paramName = inferParamFromMap(expr) ?? guessFirstParamName(expr) ?? "slug";
16343
+ return { [paramName]: new Set(keys) };
16344
+ }
16345
+ const identMapM = /^([A-Za-z_$][\w$]*)\s*\.\s*map\s*\(/.exec(expr);
16346
+ if (identMapM) {
16347
+ const ident = identMapM[1];
16348
+ const resolved = resolveIdentifierAsArray(ident, fileContent, filePath, appDir, aliasMap, visited, depth);
16349
+ if (!resolved)
16350
+ return null;
16351
+ if (resolved.kind === "strings") {
16352
+ const paramName = inferParamFromMap(expr) ?? "slug";
16353
+ return { [paramName]: new Set(resolved.values) };
16354
+ }
16355
+ if (resolved.kind === "objects") {
16356
+ const projection = extractMapProjection(expr);
16357
+ if (!projection)
16358
+ return null;
16359
+ const out = {};
16360
+ for (const [param, source] of Object.entries(projection)) {
16361
+ const values = [];
16362
+ for (const obj of resolved.values) {
16363
+ const parts = source.split(".");
16364
+ parts.shift();
16365
+ let v = obj;
16366
+ for (const p of parts) {
16367
+ if (v && typeof v === "object" && p in v) {
16368
+ v = v[p];
16369
+ } else {
16370
+ v = undefined;
16371
+ break;
16372
+ }
16373
+ }
16374
+ if (typeof v === "string")
16375
+ values.push(v);
16376
+ }
16377
+ if (values.length > 0)
16378
+ out[param] = new Set(values);
16379
+ }
16380
+ return Object.keys(out).length > 0 ? out : null;
16381
+ }
16382
+ if (resolved.kind === "objectKeys") {
16383
+ const paramName = inferParamFromMap(expr) ?? "slug";
16384
+ return { [paramName]: new Set(resolved.values) };
16385
+ }
16386
+ }
16387
+ return null;
16388
+ }
16389
+ function extractMapProjection(expr) {
16390
+ const bodyMatch = /\.map\s*\(\s*(?:\(([^)]*)\)|([A-Za-z_$][\w$]*))\s*=>\s*\(?\s*\{([^}]+)\}/.exec(expr);
16391
+ if (!bodyMatch)
16392
+ return null;
16393
+ const iter = (bodyMatch[1] ?? bodyMatch[2] ?? "").trim().split(",")[0].trim() || "item";
16394
+ const objBody = bodyMatch[3];
16395
+ const out = {};
16396
+ const propRe = /([A-Za-z_$][\w$]*)\s*(?::\s*([A-Za-z_$][\w$.]*))?/g;
16397
+ let m;
16398
+ while ((m = propRe.exec(objBody)) !== null) {
16399
+ const key = m[1];
16400
+ const value = m[2] ?? key;
16401
+ out[key] = value.startsWith(iter + ".") || value === iter ? value : `${iter}.${value}`;
16402
+ }
16403
+ return Object.keys(out).length > 0 ? out : null;
16404
+ }
16405
+ function inferParamFromMap(expr) {
16406
+ const m = /=>\s*\(?\s*\{\s*([A-Za-z_$][\w$]*)\s*[:}]/.exec(expr);
16407
+ return m ? m[1] : null;
16408
+ }
16409
+ function guessFirstParamName(expr) {
16410
+ const m = /\{\s*([A-Za-z_$][\w$]*)\s*\}/.exec(expr);
16411
+ return m ? m[1] : null;
16412
+ }
16413
+ function mergeParams(items) {
16414
+ const out = {};
16415
+ for (const item of items) {
16416
+ for (const [k, v] of Object.entries(item)) {
16417
+ if (typeof v !== "string")
16418
+ continue;
16419
+ if (!out[k])
16420
+ out[k] = new Set;
16421
+ out[k].add(v);
16422
+ }
16423
+ }
16424
+ return Object.keys(out).length > 0 ? out : null;
16425
+ }
16426
+ function parseObjectArray(expr) {
16427
+ const arrText = sliceTopLevelArray(expr);
16428
+ if (!arrText)
16429
+ return null;
16430
+ const inner = arrText.slice(1, -1).trim();
16431
+ if (!inner)
16432
+ return [];
16433
+ const items = [];
16434
+ let depth = 0;
16435
+ let buf = "";
16436
+ const chunks = [];
16437
+ let inString = null;
16438
+ for (let i = 0;i < inner.length; i++) {
16439
+ const c = inner[i];
16440
+ if (inString) {
16441
+ if (c === "\\") {
16442
+ buf += c + inner[++i];
16443
+ continue;
16444
+ }
16445
+ if (c === inString)
16446
+ inString = null;
16447
+ buf += c;
16448
+ continue;
16449
+ }
16450
+ if (c === '"' || c === "'" || c === "`") {
16451
+ inString = c;
16452
+ buf += c;
16453
+ continue;
16454
+ }
16455
+ if (c === "{" || c === "[" || c === "(")
16456
+ depth++;
16457
+ else if (c === "}" || c === "]" || c === ")")
16458
+ depth--;
16459
+ if (c === "," && depth === 0) {
16460
+ chunks.push(buf);
16461
+ buf = "";
16462
+ continue;
16463
+ }
16464
+ buf += c;
16465
+ }
16466
+ if (buf.trim())
16467
+ chunks.push(buf);
16468
+ for (const chunk of chunks) {
16469
+ const obj = parseSimpleObject(chunk.trim());
16470
+ if (!obj)
16471
+ return null;
16472
+ items.push(obj);
16473
+ }
16474
+ return items;
16475
+ }
16476
+ function parseStringArray(arrText) {
16477
+ if (!arrText.startsWith("[") || !arrText.endsWith("]"))
16478
+ return null;
16479
+ const inner = arrText.slice(1, -1).trim();
16480
+ if (!inner)
16481
+ return [];
16482
+ if (/[{[]/.test(inner))
16483
+ return null;
16484
+ const out = [];
16485
+ const re = /["'`]([^"'`]*)["'`]/g;
16486
+ let m;
16487
+ let count = 0;
16488
+ while ((m = re.exec(inner)) !== null) {
16489
+ out.push(m[1]);
16490
+ count++;
16491
+ }
16492
+ const chunks = inner.split(",").filter((s) => s.trim()).length;
16493
+ if (count !== chunks)
16494
+ return null;
16495
+ return out;
16496
+ }
16497
+ function sliceTopLevelArray(expr) {
16498
+ if (expr[0] !== "[")
16499
+ return null;
16500
+ let depth = 0;
16501
+ let inString = null;
16502
+ for (let i = 0;i < expr.length; i++) {
16503
+ const c = expr[i];
16504
+ if (inString) {
16505
+ if (c === "\\") {
16506
+ i++;
16507
+ continue;
16508
+ }
16509
+ if (c === inString)
16510
+ inString = null;
16511
+ continue;
16512
+ }
16513
+ if (c === '"' || c === "'" || c === "`") {
16514
+ inString = c;
16515
+ continue;
16516
+ }
16517
+ if (c === "[")
16518
+ depth++;
16519
+ else if (c === "]") {
16520
+ depth--;
16521
+ if (depth === 0)
16522
+ return expr.slice(0, i + 1);
16523
+ }
16524
+ }
16525
+ return null;
16526
+ }
16527
+ function parseSimpleObject(text) {
16528
+ if (!text.startsWith("{") || !text.endsWith("}"))
16529
+ return null;
16530
+ const body = text.slice(1, -1);
16531
+ const out = {};
16532
+ const re = /([A-Za-z_$][\w$]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/g;
16533
+ let m;
16534
+ let count = 0;
16535
+ while ((m = re.exec(body)) !== null) {
16536
+ out[m[1]] = m[2] ?? m[3] ?? m[4] ?? "";
16537
+ count++;
16538
+ }
16539
+ return count > 0 ? out : null;
16540
+ }
16541
+ function resolveIdentifierKeys(ident, fileContent, filePath, appDir, aliasMap, visited, depth) {
16542
+ const r = resolveIdentifier(ident, fileContent, filePath, appDir, aliasMap, visited, depth);
16543
+ if (!r)
16544
+ return null;
16545
+ if (r.kind === "objectKeys")
16546
+ return r.values;
16547
+ if (r.kind === "objects") {
16548
+ return null;
16549
+ }
16550
+ if (r.kind === "strings")
16551
+ return r.values;
16552
+ return null;
16553
+ }
16554
+ function resolveIdentifierAsArray(ident, fileContent, filePath, appDir, aliasMap, visited, depth) {
16555
+ return resolveIdentifier(ident, fileContent, filePath, appDir, aliasMap, visited, depth);
16556
+ }
16557
+ function resolveIdentifier(ident, fileContent, filePath, appDir, aliasMap, visited, depth) {
16558
+ if (depth > 5)
16559
+ return null;
16560
+ const visitKey = `${filePath}::${ident}`;
16561
+ if (visited.has(visitKey))
16562
+ return null;
16563
+ visited.add(visitKey);
16564
+ const localValue = findLocalConst(ident, fileContent);
16565
+ if (localValue) {
16566
+ const parsed = parseLiteralValue(localValue);
16567
+ if (parsed)
16568
+ return parsed;
16569
+ }
16570
+ const importInfo = findImport(ident, fileContent);
16571
+ if (!importInfo)
16572
+ return null;
16573
+ const resolvedPath = resolveModulePath(importInfo.path, filePath, appDir, aliasMap);
16574
+ if (!resolvedPath)
16575
+ return null;
16576
+ if (resolvedPath.endsWith(JSON_EXT)) {
16577
+ try {
16578
+ const data = JSON.parse(readFileSync9(resolvedPath, "utf-8"));
16579
+ return literalToResolution(data);
16580
+ } catch {
16581
+ return null;
16582
+ }
16583
+ }
16584
+ let nextContent;
16585
+ try {
16586
+ nextContent = readFileSync9(resolvedPath, "utf-8");
16587
+ } catch {
16588
+ return null;
16589
+ }
16590
+ const nextIdent = importInfo.kind === "default" ? findDefaultExportIdentifier(nextContent) ?? ident : importInfo.imported;
16591
+ return resolveIdentifier(nextIdent, nextContent, resolvedPath, appDir, aliasMap, visited, depth + 1);
16592
+ }
16593
+ function findImport(ident, source) {
16594
+ const defRe = new RegExp(`import\\s+${ident}\\s+from\\s+['"]([^'"]+)['"]`, "g");
16595
+ let m;
16596
+ if ((m = defRe.exec(source)) !== null) {
16597
+ return { kind: "default", imported: ident, path: m[1] };
16598
+ }
16599
+ const namedRe = /import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g;
16600
+ while ((m = namedRe.exec(source)) !== null) {
16601
+ const specifiers = m[1].split(",").map((s) => s.trim());
16602
+ for (const spec of specifiers) {
16603
+ const aliasMatch = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(spec);
16604
+ if (aliasMatch) {
16605
+ if (aliasMatch[2] === ident) {
16606
+ return { kind: "named", imported: aliasMatch[1], path: m[2] };
16607
+ }
16608
+ } else if (spec === ident) {
16609
+ return { kind: "named", imported: ident, path: m[2] };
16610
+ }
16611
+ }
16612
+ }
16613
+ return null;
16614
+ }
16615
+ function resolveModulePath(spec, fromFile, appDir, aliasMap) {
16616
+ let candidates = [];
16617
+ if (spec.startsWith(".")) {
16618
+ candidates.push(resolve3(dirname5(fromFile), spec));
16619
+ } else if (aliasMap.size > 0) {
16620
+ for (const [prefix, targets] of aliasMap.entries()) {
16621
+ if (spec === prefix.replace(/\/$/, "") || spec.startsWith(prefix)) {
16622
+ const sub = spec.slice(prefix.length);
16623
+ for (const t of targets) {
16624
+ candidates.push(sub ? join7(t, sub) : t);
16625
+ }
16626
+ }
16627
+ }
16628
+ } else {
16629
+ candidates.push(join7(appDir, spec));
16630
+ }
16631
+ for (const cand of candidates) {
16632
+ if (existsSync7(cand)) {
16633
+ try {
16634
+ if (statSync2(cand).isFile())
16635
+ return cand;
16636
+ } catch {}
16637
+ }
16638
+ for (const ext2 of [...TS_EXTS, JSON_EXT]) {
16639
+ if (existsSync7(cand + ext2))
16640
+ return cand + ext2;
16641
+ }
16642
+ for (const ext2 of TS_EXTS) {
16643
+ const idx = join7(cand, "index" + ext2);
16644
+ if (existsSync7(idx))
16645
+ return idx;
16646
+ }
16647
+ }
16648
+ return null;
16649
+ }
16650
+ function findLocalConst(ident, source) {
16651
+ const re = new RegExp(`(?:export\\s+)?const\\s+${ident}\\s*(?::[^=]+)?=\\s*`, "g");
16652
+ const m = re.exec(source);
16653
+ if (!m)
16654
+ return null;
16655
+ const start = m.index + m[0].length;
16656
+ return readExpression(source, start);
16657
+ }
16658
+ function readExpression(source, start) {
16659
+ let depth = 0;
16660
+ let inString = null;
16661
+ let i = start;
16662
+ for (;i < source.length; i++) {
16663
+ const c = source[i];
16664
+ if (inString) {
16665
+ if (c === "\\") {
16666
+ i++;
16667
+ continue;
16668
+ }
16669
+ if (c === inString)
16670
+ inString = null;
16671
+ continue;
16672
+ }
16673
+ if (c === '"' || c === "'" || c === "`") {
16674
+ inString = c;
16675
+ continue;
16676
+ }
16677
+ if (c === "{" || c === "[" || c === "(")
16678
+ depth++;
16679
+ else if (c === "}" || c === "]" || c === ")")
16680
+ depth--;
16681
+ if (depth === 0 && (c === ";" || c === `
16682
+ `))
16683
+ break;
16684
+ }
16685
+ return source.slice(start, i).trim().replace(/[;]+$/, "").trim();
16686
+ }
16687
+ function findDefaultExportIdentifier(source) {
16688
+ const m = /export\s+default\s+([A-Za-z_$][\w$]*)\s*;?/.exec(source);
16689
+ return m ? m[1] : null;
16690
+ }
16691
+ function parseLiteralValue(expr) {
16692
+ expr = expr.trim();
16693
+ if (expr.startsWith("{")) {
16694
+ const keys = extractTopLevelKeys(expr);
16695
+ if (keys.length > 0)
16696
+ return { kind: "objectKeys", values: keys };
16697
+ return null;
16698
+ }
16699
+ if (expr.startsWith("[")) {
16700
+ const arrText = sliceTopLevelArray(expr);
16701
+ if (!arrText)
16702
+ return null;
16703
+ const stringRe = /["'`]([^"'`]*)["'`]/g;
16704
+ const inner = arrText.slice(1, -1);
16705
+ if (/^\s*\{/.test(inner)) {
16706
+ const objs = parseObjectArray(arrText);
16707
+ if (objs)
16708
+ return { kind: "objects", values: objs };
16709
+ return null;
16710
+ }
16711
+ const values = [];
16712
+ let m;
16713
+ while ((m = stringRe.exec(inner)) !== null)
16714
+ values.push(m[1]);
16715
+ if (values.length > 0)
16716
+ return { kind: "strings", values };
16717
+ }
16718
+ return null;
16719
+ }
16720
+ function extractTopLevelKeys(expr) {
16721
+ const inner = expr.slice(1, expr.length - 1);
16722
+ const keys = [];
16723
+ let depth = 0;
16724
+ let i = 0;
16725
+ let inString = null;
16726
+ let atKeyPos = true;
16727
+ while (i < inner.length) {
16728
+ const c = inner[i];
16729
+ if (inString) {
16730
+ if (c === "\\") {
16731
+ i += 2;
16732
+ continue;
16733
+ }
16734
+ if (c === inString)
16735
+ inString = null;
16736
+ i++;
16737
+ continue;
16738
+ }
16739
+ if (c === '"' || c === "'" || c === "`") {
16740
+ if (atKeyPos && depth === 0) {
16741
+ const quote = c;
16742
+ const end = inner.indexOf(quote, i + 1);
16743
+ if (end === -1)
16744
+ break;
16745
+ keys.push(inner.slice(i + 1, end));
16746
+ i = end + 1;
16747
+ while (i < inner.length && /\s/.test(inner[i]))
16748
+ i++;
16749
+ if (inner[i] === ":") {
16750
+ i++;
16751
+ atKeyPos = false;
16752
+ }
16753
+ continue;
16754
+ }
16755
+ inString = c;
16756
+ i++;
16757
+ continue;
16758
+ }
16759
+ if (c === "{" || c === "[" || c === "(") {
16760
+ depth++;
16761
+ i++;
16762
+ continue;
16763
+ }
16764
+ if (c === "}" || c === "]" || c === ")") {
16765
+ depth--;
16766
+ i++;
16767
+ continue;
16768
+ }
16769
+ if (depth === 0 && c === ",") {
16770
+ atKeyPos = true;
16771
+ i++;
16772
+ continue;
16773
+ }
16774
+ if (depth === 0 && c === ":") {
16775
+ atKeyPos = false;
16776
+ i++;
16777
+ continue;
16778
+ }
16779
+ if (atKeyPos && depth === 0 && /[A-Za-z_$]/.test(c)) {
16780
+ let j = i;
16781
+ while (j < inner.length && /[\w$]/.test(inner[j]))
16782
+ j++;
16783
+ const key = inner.slice(i, j);
16784
+ let k = j;
16785
+ while (k < inner.length && /\s/.test(inner[k]))
16786
+ k++;
16787
+ if (inner[k] === ":") {
16788
+ keys.push(key);
16789
+ i = k + 1;
16790
+ atKeyPos = false;
16791
+ continue;
16792
+ }
16793
+ i = j;
16794
+ continue;
16795
+ }
16796
+ i++;
16797
+ }
16798
+ return keys;
16799
+ }
16800
+ function literalToResolution(data) {
16801
+ if (Array.isArray(data)) {
16802
+ if (data.every((v) => typeof v === "string")) {
16803
+ return { kind: "strings", values: data };
16804
+ }
16805
+ if (data.every((v) => v && typeof v === "object")) {
16806
+ return { kind: "objects", values: data };
16807
+ }
16808
+ return null;
16809
+ }
16810
+ if (data && typeof data === "object") {
16811
+ return { kind: "objectKeys", values: Object.keys(data) };
16812
+ }
16813
+ return null;
16814
+ }
16132
16815
 
16133
16816
  // src/scanner.ts
16134
16817
  function extractRoutePath(filePath) {
@@ -16634,7 +17317,7 @@ async function scan(config) {
16634
17317
  // src/config.ts
16635
17318
  var import_fast_glob11 = __toESM(require_out4(), 1);
16636
17319
  import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
16637
- import { join as join9, resolve as resolve3, relative as relative4, dirname as dirname5 } from "node:path";
17320
+ import { join as join9, resolve as resolve4, relative as relative4, dirname as dirname6 } from "node:path";
16638
17321
  var DEFAULT_CONFIG = {
16639
17322
  dir: "./",
16640
17323
  ignore: {
@@ -16672,7 +17355,7 @@ function loadConfig(options) {
16672
17355
  absolute: true
16673
17356
  });
16674
17357
  if (options.config && existsSync9(options.config)) {
16675
- const absConfig = resolve3(cwd, options.config);
17358
+ const absConfig = resolve4(cwd, options.config);
16676
17359
  if (!configFiles.includes(absConfig)) {
16677
17360
  configFiles.push(absConfig);
16678
17361
  }
@@ -16695,7 +17378,7 @@ function loadConfig(options) {
16695
17378
  try {
16696
17379
  const content = readFileSync11(configPath, "utf-8");
16697
17380
  const config = JSON.parse(content);
16698
- const configDir = dirname5(configPath);
17381
+ const configDir = dirname6(configPath);
16699
17382
  const relDir = relative4(cwd, configDir);
16700
17383
  const prefixPattern = (p) => {
16701
17384
  if (p.startsWith("**/") || p.startsWith("/") || !relDir)
@@ -16810,7 +17493,7 @@ program2.action(async (options) => {
16810
17493
  let isMonorepo = existsSync11(appsDir) && lstatSync(appsDir).isDirectory();
16811
17494
  if (!isMonorepo) {
16812
17495
  let current = absoluteDir;
16813
- while (current !== dirname6(current)) {
17496
+ while (current !== dirname7(current)) {
16814
17497
  const potentialApps = join11(current, "apps");
16815
17498
  if (existsSync11(potentialApps) && lstatSync(potentialApps).isDirectory()) {
16816
17499
  monorepoRoot = current;
@@ -16818,7 +17501,7 @@ program2.action(async (options) => {
16818
17501
  isMonorepo = true;
16819
17502
  break;
16820
17503
  }
16821
- current = dirname6(current);
17504
+ current = dirname7(current);
16822
17505
  }
16823
17506
  }
16824
17507
  if (isMonorepo && monorepoRoot !== absoluteDir) {
@@ -16919,6 +17602,10 @@ program2.action(async (options) => {
16919
17602
  if (options.json) {
16920
17603
  console.log(JSON.stringify(result, null, 2));
16921
17604
  } else if (options.fix) {
17605
+ if (isScanAll && !hasUnusedItems(result)) {
17606
+ console.log(source_default.green(` ✅ Nothing to fix in ${appLabel}`));
17607
+ continue;
17608
+ }
16922
17609
  const fixResult = await handleFixes(result, currentConfig, options, isMonorepo);
16923
17610
  if (fixResult === "back") {
16924
17611
  requestedBack = true;
@@ -17161,7 +17848,7 @@ Analyzing cascading impact...`));
17161
17848
  const routeFiles = new Set;
17162
17849
  const rootDir = config.appSpecificScan ? config.appSpecificScan.rootDir : config.dir;
17163
17850
  for (const r of [...unusedRoutes, ...partiallyRoutes]) {
17164
- routeFiles.add(resolve4(rootDir, r.filePath));
17851
+ routeFiles.add(resolve5(rootDir, r.filePath));
17165
17852
  }
17166
17853
  const title = `Unused API Routes (${totalRoutesIssues} items in ${routeFiles.size} files)`;
17167
17854
  choices.push({ title, value: "routes" });
@@ -17458,7 +18145,7 @@ Analyzing cascading impact...`));
17458
18145
  const fullPath = resolveFilePath(filePath, config);
17459
18146
  const route = fileRoutes[0];
17460
18147
  if (route.type === "nextjs" && (filePath.includes("app/api") || filePath.includes("pages/api"))) {
17461
- filesToUnlink.add(dirname6(fullPath));
18148
+ filesToUnlink.add(dirname7(fullPath));
17462
18149
  } else if (route.type === "nestjs" && (result.unusedFiles?.files.some((f) => f.path === filePath) || filePath.includes("api/"))) {
17463
18150
  filesToUnlink.add(fullPath);
17464
18151
  } else {
@@ -17512,7 +18199,7 @@ Analyzing cascading impact...`));
17512
18199
  }
17513
18200
  const actuallyFixed = new Set;
17514
18201
  for (const [absPath, removals] of removalsByFile) {
17515
- if (actuallyDeleted.has(absPath) || actuallyDeleted.has(dirname6(absPath)))
18202
+ if (actuallyDeleted.has(absPath) || actuallyDeleted.has(dirname7(absPath)))
17516
18203
  continue;
17517
18204
  if (!existsSync11(absPath))
17518
18205
  continue;
@@ -17527,7 +18214,7 @@ Analyzing cascading impact...`));
17527
18214
  }
17528
18215
  result.routes = result.routes.filter((r) => {
17529
18216
  const fullPath = resolveFilePath(r.filePath, config);
17530
- if (actuallyDeleted.has(fullPath) || actuallyDeleted.has(dirname6(fullPath)))
18217
+ if (actuallyDeleted.has(fullPath) || actuallyDeleted.has(dirname7(fullPath)))
17531
18218
  return false;
17532
18219
  if (actuallyFixed.has(fullPath)) {
17533
18220
  r.unusedMethods = [];
@@ -17999,11 +18686,11 @@ function printTable(summary) {
17999
18686
  }
18000
18687
  function findGitRoot(startDir) {
18001
18688
  let currentDir = startDir;
18002
- while (currentDir !== dirname6(currentDir)) {
18689
+ while (currentDir !== dirname7(currentDir)) {
18003
18690
  if (existsSync11(join11(currentDir, ".git"))) {
18004
18691
  return true;
18005
18692
  }
18006
- currentDir = dirname6(currentDir);
18693
+ currentDir = dirname7(currentDir);
18007
18694
  }
18008
18695
  return false;
18009
18696
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.43.10",
3
+ "version": "1.44.1",
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",