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.
Files changed (2) hide show
  1. package/dist/index.js +675 -21
  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,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
- 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
+ }
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
- 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
+ }
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 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";
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 = resolve3(cwd, options.config);
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 = dirname5(configPath);
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 !== dirname6(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 = dirname6(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(resolve4(rootDir, r.filePath));
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(dirname6(fullPath));
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(dirname6(absPath)))
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(dirname6(fullPath)))
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 !== dirname6(currentDir)) {
18656
+ while (currentDir !== dirname7(currentDir)) {
18003
18657
  if (existsSync11(join11(currentDir, ".git"))) {
18004
18658
  return true;
18005
18659
  }
18006
- currentDir = dirname6(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.43.10",
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",