pruny 1.36.0 → 1.37.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 +305 -23
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,15 +5,29 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
8
13
  var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
9
21
  target = mod != null ? __create(__getProtoOf(mod)) : {};
10
22
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
23
  for (let key of __getOwnPropNames(mod))
12
24
  if (!__hasOwnProp.call(to, key))
13
25
  __defProp(to, key, {
14
- get: () => mod[key],
26
+ get: __accessProp.bind(mod, key),
15
27
  enumerable: true
16
28
  });
29
+ if (canCache)
30
+ cache.set(mod, to);
17
31
  return to;
18
32
  };
19
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -12553,8 +12567,8 @@ import { rmSync, existsSync as existsSync9, readdirSync, lstatSync, writeFileSyn
12553
12567
  import { dirname as dirname5, join as join10, relative as relative5, resolve as resolve3 } from "node:path";
12554
12568
 
12555
12569
  // src/scanner.ts
12556
- var import_fast_glob9 = __toESM(require_out4(), 1);
12557
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
12570
+ var import_fast_glob10 = __toESM(require_out4(), 1);
12571
+ import { existsSync as existsSync6, readFileSync as readFileSync9 } from "node:fs";
12558
12572
  import { join as join7 } from "node:path";
12559
12573
 
12560
12574
  // src/patterns.ts
@@ -15715,6 +15729,176 @@ async function scanUnusedServices(config) {
15715
15729
  return { total: methods.length, methods };
15716
15730
  }
15717
15731
 
15732
+ // src/scanners/broken-links.ts
15733
+ var import_fast_glob9 = __toESM(require_out4(), 1);
15734
+ import { readFileSync as readFileSync8 } from "node:fs";
15735
+ var LINK_PATTERNS = [
15736
+ /<Link\s+[^>]*href\s*=\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15737
+ /router\.(push|replace)\s*\(\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15738
+ /(?:redirect|permanentRedirect)\s*\(\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15739
+ /href\s*:\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15740
+ /<a\s+[^>]*href\s*=\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15741
+ /revalidatePath\s*\(\s*['"`](\/[^'"`\s{}$]+)['"`]/g,
15742
+ /pathname\s*===?\s*['"`](\/[^'"`\s{}$]+)['"`]/g
15743
+ ];
15744
+ function extractPath(match2) {
15745
+ if (match2[2] && match2[2].startsWith("/"))
15746
+ return match2[2];
15747
+ if (match2[1] && match2[1].startsWith("/"))
15748
+ return match2[1];
15749
+ return null;
15750
+ }
15751
+ function shouldSkipPath(path2) {
15752
+ if (/^https?:\/\//.test(path2))
15753
+ return true;
15754
+ if (/^mailto:/.test(path2))
15755
+ return true;
15756
+ if (/^tel:/.test(path2))
15757
+ return true;
15758
+ if (path2 === "#" || path2.startsWith("#"))
15759
+ return true;
15760
+ if (path2.startsWith("/api/") || path2 === "/api")
15761
+ return true;
15762
+ if (path2 === "/_next" || path2.startsWith("/_next/"))
15763
+ return true;
15764
+ return false;
15765
+ }
15766
+ function cleanPath(path2) {
15767
+ return path2.replace(/[?#].*$/, "").replace(/\/$/, "") || "/";
15768
+ }
15769
+ function filePathToRoute(filePath) {
15770
+ let path2 = filePath.replace(/^src\//, "").replace(/^apps\/[^/]+\//, "").replace(/^packages\/[^/]+\//, "");
15771
+ path2 = path2.replace(/^app\//, "").replace(/^pages\//, "");
15772
+ path2 = path2.replace(/\/page\.(ts|tsx|js|jsx|md|mdx)$/, "");
15773
+ path2 = path2.replace(/\.(ts|tsx|js|jsx)$/, "");
15774
+ path2 = path2.replace(/\/index$/, "");
15775
+ const segments = path2.split("/").filter((segment) => {
15776
+ if (/^\([^.)][^)]*\)$/.test(segment))
15777
+ return false;
15778
+ if (segment.startsWith("@"))
15779
+ return false;
15780
+ if (/^\(\.+\)/.test(segment))
15781
+ return false;
15782
+ return true;
15783
+ });
15784
+ return "/" + segments.join("/");
15785
+ }
15786
+ function matchesRoute(refPath, routes, routeSegments) {
15787
+ const cleaned = cleanPath(refPath);
15788
+ if (routes.has(cleaned))
15789
+ return true;
15790
+ const refSegments = cleaned.split("/").filter(Boolean);
15791
+ for (const routeSeg of routeSegments) {
15792
+ if (matchSegments(refSegments, routeSeg))
15793
+ return true;
15794
+ }
15795
+ return false;
15796
+ }
15797
+ function matchSegments(refSegments, routeSegments) {
15798
+ let ri = 0;
15799
+ let si = 0;
15800
+ while (ri < refSegments.length && si < routeSegments.length) {
15801
+ const routeSeg = routeSegments[si];
15802
+ if (/^\[\[?\.\.\./.test(routeSeg))
15803
+ return true;
15804
+ if (/^\[.+\]$/.test(routeSeg)) {
15805
+ ri++;
15806
+ si++;
15807
+ continue;
15808
+ }
15809
+ if (refSegments[ri].toLowerCase() !== routeSeg.toLowerCase())
15810
+ return false;
15811
+ ri++;
15812
+ si++;
15813
+ }
15814
+ return ri === refSegments.length && si === routeSegments.length;
15815
+ }
15816
+ async function scanBrokenLinks(config) {
15817
+ const appDir = config.appSpecificScan ? config.appSpecificScan.appDir : config.dir;
15818
+ const pagePatterns = [
15819
+ "app/**/page.{ts,tsx,js,jsx,md,mdx}",
15820
+ "src/app/**/page.{ts,tsx,js,jsx,md,mdx}",
15821
+ "pages/**/*.{ts,tsx,js,jsx}",
15822
+ "src/pages/**/*.{ts,tsx,js,jsx}"
15823
+ ];
15824
+ const pageFiles = await import_fast_glob9.default(pagePatterns, {
15825
+ cwd: appDir,
15826
+ ignore: [...config.ignore.folders, "**/node_modules/**", "**/_*/**"]
15827
+ });
15828
+ if (pageFiles.length === 0) {
15829
+ return { total: 0, links: [] };
15830
+ }
15831
+ const knownRoutes = new Set;
15832
+ const routeSegmentsList = [];
15833
+ knownRoutes.add("/");
15834
+ for (const file of pageFiles) {
15835
+ const route = filePathToRoute(file);
15836
+ knownRoutes.add(route);
15837
+ const segments = route.split("/").filter(Boolean);
15838
+ if (segments.some((s) => s.startsWith("["))) {
15839
+ routeSegmentsList.push(segments);
15840
+ }
15841
+ }
15842
+ if (process.env.DEBUG_PRUNY) {
15843
+ console.log(`[DEBUG] Known routes: ${Array.from(knownRoutes).join(", ")}`);
15844
+ }
15845
+ const refDir = config.appSpecificScan ? config.appSpecificScan.rootDir : config.dir;
15846
+ const ignore = [...config.ignore.folders, ...config.ignore.files, "**/node_modules/**"];
15847
+ const extensions = config.extensions;
15848
+ const globPattern = `**/*{${extensions.join(",")}}`;
15849
+ const sourceFiles = await import_fast_glob9.default(globPattern, {
15850
+ cwd: refDir,
15851
+ ignore,
15852
+ absolute: true
15853
+ });
15854
+ const brokenMap = new Map;
15855
+ for (const file of sourceFiles) {
15856
+ try {
15857
+ const content = readFileSync8(file, "utf-8");
15858
+ for (const pattern of LINK_PATTERNS) {
15859
+ pattern.lastIndex = 0;
15860
+ let match2;
15861
+ while ((match2 = pattern.exec(content)) !== null) {
15862
+ const rawPath = extractPath(match2);
15863
+ if (!rawPath)
15864
+ continue;
15865
+ if (shouldSkipPath(rawPath))
15866
+ continue;
15867
+ const cleaned = cleanPath(rawPath);
15868
+ if (!cleaned || cleaned === "/")
15869
+ continue;
15870
+ if (!matchesRoute(cleaned, knownRoutes, routeSegmentsList)) {
15871
+ const isIgnored = config.ignore.routes.some((ignorePath) => {
15872
+ const pattern2 = ignorePath.replace(/\*/g, ".*");
15873
+ return new RegExp(`^${pattern2}$`).test(cleaned);
15874
+ });
15875
+ if (isIgnored)
15876
+ continue;
15877
+ const lineNumber = content.substring(0, match2.index).split(`
15878
+ `).length;
15879
+ if (!brokenMap.has(cleaned)) {
15880
+ brokenMap.set(cleaned, new Set);
15881
+ }
15882
+ brokenMap.get(cleaned).add(`${file}:${lineNumber}`);
15883
+ }
15884
+ }
15885
+ }
15886
+ } catch (_e) {}
15887
+ }
15888
+ const links = [];
15889
+ for (const [path2, refs] of brokenMap.entries()) {
15890
+ links.push({
15891
+ path: path2,
15892
+ references: Array.from(refs).sort()
15893
+ });
15894
+ }
15895
+ links.sort((a, b) => b.references.length - a.references.length);
15896
+ return {
15897
+ total: links.length,
15898
+ links
15899
+ };
15900
+ }
15901
+
15718
15902
  // src/scanner.ts
15719
15903
  function extractRoutePath(filePath) {
15720
15904
  let path2 = filePath.replace(/^src\//, "").replace(/^apps\/[^/]+\//, "").replace(/^packages\/[^/]+\//, "");
@@ -15820,19 +16004,19 @@ function extractNestMethodName(content) {
15820
16004
  return "";
15821
16005
  }
15822
16006
  function shouldIgnore(path2, ignorePatterns) {
15823
- const cleanPath = path2.replace(/\\/g, "/").replace(/^\//, "").replace(/^\.\//, "");
16007
+ const cleanPath2 = path2.replace(/\\/g, "/").replace(/^\//, "").replace(/^\.\//, "");
15824
16008
  return ignorePatterns.some((pattern) => {
15825
16009
  let cleanPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
15826
16010
  const isAbsolute4 = cleanPattern.startsWith("/");
15827
16011
  if (isAbsolute4)
15828
16012
  cleanPattern = cleanPattern.substring(1);
15829
- if (minimatch(cleanPath, cleanPattern))
16013
+ if (minimatch(cleanPath2, cleanPattern))
15830
16014
  return true;
15831
16015
  const folderPattern = cleanPattern.endsWith("/") ? cleanPattern : cleanPattern + "/";
15832
- if (cleanPath.startsWith(folderPattern))
16016
+ if (cleanPath2.startsWith(folderPattern))
15833
16017
  return true;
15834
16018
  if (!isAbsolute4 && !cleanPattern.includes("/") && !cleanPattern.includes("*")) {
15835
- if (cleanPath.endsWith("/" + cleanPattern) || cleanPath === cleanPattern)
16019
+ if (cleanPath2.endsWith("/" + cleanPattern) || cleanPath2 === cleanPattern)
15836
16020
  return true;
15837
16021
  }
15838
16022
  return false;
@@ -15849,9 +16033,9 @@ async function detectGlobalPrefix(appDir) {
15849
16033
  const mainTsAltPath = join7(appDir, "main.ts");
15850
16034
  let content;
15851
16035
  if (existsSync6(mainTsPath)) {
15852
- content = readFileSync8(mainTsPath, "utf-8");
16036
+ content = readFileSync9(mainTsPath, "utf-8");
15853
16037
  } else if (existsSync6(mainTsAltPath)) {
15854
- content = readFileSync8(mainTsAltPath, "utf-8");
16038
+ content = readFileSync9(mainTsAltPath, "utf-8");
15855
16039
  } else {
15856
16040
  return "";
15857
16041
  }
@@ -15913,7 +16097,7 @@ function getVercelCronPaths(dir) {
15913
16097
  return [];
15914
16098
  }
15915
16099
  try {
15916
- const content = readFileSync8(vercelPath, "utf-8");
16100
+ const content = readFileSync9(vercelPath, "utf-8");
15917
16101
  const config = JSON.parse(content);
15918
16102
  if (!config.crons) {
15919
16103
  return [];
@@ -15949,13 +16133,13 @@ async function scan(config) {
15949
16133
  if (prefix)
15950
16134
  detectedGlobalPrefix = prefix;
15951
16135
  }
15952
- const nextFiles = await import_fast_glob9.default(activeNextPatterns, {
16136
+ const nextFiles = await import_fast_glob10.default(activeNextPatterns, {
15953
16137
  cwd: scanCwd,
15954
16138
  ignore: config.ignore.folders
15955
16139
  });
15956
16140
  const nextRoutes = nextFiles.map((file) => {
15957
16141
  const fullPath = join7(scanCwd, file);
15958
- const content = readFileSync8(fullPath, "utf-8");
16142
+ const content = readFileSync9(fullPath, "utf-8");
15959
16143
  const { methods, methodLines } = extractExportedMethods(content);
15960
16144
  return {
15961
16145
  type: "nextjs",
@@ -15969,13 +16153,13 @@ async function scan(config) {
15969
16153
  };
15970
16154
  });
15971
16155
  const nestPatterns = ["**/*.controller.ts"];
15972
- const nestFiles = await import_fast_glob9.default(nestPatterns, {
16156
+ const nestFiles = await import_fast_glob10.default(nestPatterns, {
15973
16157
  cwd: scanCwd,
15974
16158
  ignore: config.ignore.folders
15975
16159
  });
15976
16160
  const nestRoutes = nestFiles.flatMap((file) => {
15977
16161
  const fullPath = join7(scanCwd, file);
15978
- const content = readFileSync8(fullPath, "utf-8");
16162
+ const content = readFileSync9(fullPath, "utf-8");
15979
16163
  const relativePathFromRoot = fullPath.replace(config.appSpecificScan ? config.appSpecificScan.rootDir + "/" : cwd + "/", "");
15980
16164
  return extractNestRoutes(relativePathFromRoot, content, detectedGlobalPrefix);
15981
16165
  });
@@ -15995,7 +16179,7 @@ async function scan(config) {
15995
16179
  }
15996
16180
  const referenceScanCwd = config.appSpecificScan ? config.appSpecificScan.rootDir : cwd;
15997
16181
  const extGlob = `**/*{${config.extensions.join(",")}}`;
15998
- const sourceFiles = await import_fast_glob9.default(extGlob, {
16182
+ const sourceFiles = await import_fast_glob10.default(extGlob, {
15999
16183
  cwd: referenceScanCwd,
16000
16184
  ignore: [...config.ignore.folders, ...config.ignore.files]
16001
16185
  });
@@ -16004,7 +16188,7 @@ async function scan(config) {
16004
16188
  for (const file of sourceFiles) {
16005
16189
  const filePath = join7(referenceScanCwd, file);
16006
16190
  try {
16007
- const content = readFileSync8(filePath, "utf-8");
16191
+ const content = readFileSync9(filePath, "utf-8");
16008
16192
  const refs = extractApiReferences(content);
16009
16193
  if (refs.length > 0) {
16010
16194
  fileReferences.set(file, refs);
@@ -16051,6 +16235,7 @@ async function scan(config) {
16051
16235
  routes,
16052
16236
  publicAssets,
16053
16237
  missingAssets: await scanMissingAssets(config),
16238
+ brokenLinks: await scanBrokenLinks(config),
16054
16239
  unusedFiles,
16055
16240
  unusedExports: await scanUnusedExports(config).then((result) => {
16056
16241
  const filtered = result.exports.filter((exp) => !exp.file.endsWith(".controller.ts") && !exp.file.endsWith(".controller.tsx"));
@@ -16062,8 +16247,8 @@ async function scan(config) {
16062
16247
  }
16063
16248
 
16064
16249
  // src/config.ts
16065
- var import_fast_glob10 = __toESM(require_out4(), 1);
16066
- import { existsSync as existsSync7, readFileSync as readFileSync9 } from "node:fs";
16250
+ var import_fast_glob11 = __toESM(require_out4(), 1);
16251
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "node:fs";
16067
16252
  import { join as join8, resolve as resolve2, relative as relative4, dirname as dirname4 } from "node:path";
16068
16253
  var DEFAULT_CONFIG = {
16069
16254
  dir: "./",
@@ -16086,7 +16271,7 @@ var DEFAULT_CONFIG = {
16086
16271
  };
16087
16272
  function loadConfig(options) {
16088
16273
  const cwd = options.dir || "./";
16089
- const configFiles = import_fast_glob10.default.sync(["**/pruny.config.json", "**/.prunyrc.json", "**/.prunyrc"], {
16274
+ const configFiles = import_fast_glob11.default.sync(["**/pruny.config.json", "**/.prunyrc.json", "**/.prunyrc"], {
16090
16275
  cwd,
16091
16276
  ignore: DEFAULT_CONFIG.ignore.folders,
16092
16277
  absolute: true
@@ -16112,7 +16297,7 @@ function loadConfig(options) {
16112
16297
  let excludePublic = options.excludePublic ?? false;
16113
16298
  for (const configPath of configFiles) {
16114
16299
  try {
16115
- const content = readFileSync9(configPath, "utf-8");
16300
+ const content = readFileSync10(configPath, "utf-8");
16116
16301
  const config = JSON.parse(content);
16117
16302
  const configDir = dirname4(configPath);
16118
16303
  const relDir = relative4(cwd, configDir);
@@ -16159,7 +16344,7 @@ function parseGitIgnore(dir) {
16159
16344
  if (!existsSync7(gitIgnorePath))
16160
16345
  return [];
16161
16346
  try {
16162
- const content = readFileSync9(gitIgnorePath, "utf-8");
16347
+ const content = readFileSync10(gitIgnorePath, "utf-8");
16163
16348
  return content.split(`
16164
16349
  `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((pattern) => {
16165
16350
  if (pattern.startsWith("/") || pattern.startsWith("**/"))
@@ -16434,6 +16619,10 @@ function filterResults(result, filterPattern) {
16434
16619
  result.unusedExports.total = result.unusedExports.exports.length;
16435
16620
  result.unusedExports.unused = result.unusedExports.exports.length;
16436
16621
  }
16622
+ if (result.brokenLinks) {
16623
+ result.brokenLinks.links = result.brokenLinks.links.filter((l) => matchesFilter(l.path, filter2));
16624
+ result.brokenLinks.total = result.brokenLinks.links.length;
16625
+ }
16437
16626
  result.total = result.routes.length;
16438
16627
  result.used = result.routes.filter((r) => r.used).length;
16439
16628
  result.unused = result.routes.filter((r) => !r.used).length;
@@ -16511,6 +16700,17 @@ function printDetailedReport(result) {
16511
16700
  }
16512
16701
  console.log("");
16513
16702
  }
16703
+ if (result.brokenLinks && result.brokenLinks.total > 0) {
16704
+ console.log(source_default.red.bold(`\uD83D\uDD17 Broken Internal Links:
16705
+ `));
16706
+ for (const link of result.brokenLinks.links) {
16707
+ console.log(source_default.red(` ${link.path}`));
16708
+ for (const ref of link.references) {
16709
+ console.log(source_default.dim(` → ${ref}`));
16710
+ }
16711
+ }
16712
+ console.log("");
16713
+ }
16514
16714
  if (!hasUnusedItems(result)) {
16515
16715
  console.log(source_default.green(`✅ Everything is used! Clean as a whistle.
16516
16716
  `));
@@ -16524,10 +16724,11 @@ function countIssues(result) {
16524
16724
  const partialRoutes = result.routes.filter((r) => r.used && r.unusedMethods.length > 0).length;
16525
16725
  const unusedAssets = result.publicAssets ? result.publicAssets.unused : 0;
16526
16726
  const missingAssets = result.missingAssets ? result.missingAssets.total : 0;
16727
+ const brokenLinks = result.brokenLinks ? result.brokenLinks.total : 0;
16527
16728
  const unusedFiles = result.unusedFiles ? result.unusedFiles.unused : 0;
16528
16729
  const unusedExports = result.unusedExports ? result.unusedExports.unused : 0;
16529
16730
  const unusedServices = result.unusedServices ? result.unusedServices.total : 0;
16530
- return unusedRoutes + partialRoutes + unusedAssets + missingAssets + unusedFiles + unusedExports + unusedServices;
16731
+ return unusedRoutes + partialRoutes + unusedAssets + missingAssets + brokenLinks + unusedFiles + unusedExports + unusedServices;
16531
16732
  }
16532
16733
  async function handleFixes(result, config, options, showBack) {
16533
16734
  const gitRoot = findGitRoot(config.dir);
@@ -16605,6 +16806,11 @@ Analyzing cascading impact...`));
16605
16806
  const title = count > 0 ? `⚠ Missing Assets (Broken Links) (${count})` : `✅ Missing Assets (0) - All good!`;
16606
16807
  choices.push({ title, value: "missing-assets" });
16607
16808
  }
16809
+ if (result.brokenLinks) {
16810
+ const count = result.brokenLinks.total;
16811
+ const title = count > 0 ? `\uD83D\uDD17 Broken Internal Links (${count})` : `✅ Internal Links (0) - All good!`;
16812
+ choices.push({ title, value: "broken-links" });
16813
+ }
16608
16814
  if (showBack) {
16609
16815
  choices.push({ title: source_default.cyan("← Back"), value: "back" });
16610
16816
  }
@@ -16671,7 +16877,11 @@ Analyzing cascading impact...`));
16671
16877
  const dryRunReport = {
16672
16878
  uniqueFiles: new Set(targetRoutes.map((r) => r.filePath)).size,
16673
16879
  routes: [],
16674
- exports: []
16880
+ exports: [],
16881
+ files: [],
16882
+ assets: [],
16883
+ missingAssets: [],
16884
+ brokenLinks: []
16675
16885
  };
16676
16886
  if (selected === "routes" || selected === "dry-run-json" || action === "dry-run") {
16677
16887
  dryRunReport.routes = targetRoutes.map((r) => ({
@@ -16718,6 +16928,38 @@ Analyzing cascading impact...`));
16718
16928
  }));
16719
16929
  dryRunReport.uniqueFiles = new Set(servicesList.map((m) => m.file)).size;
16720
16930
  }
16931
+ if (selected === "files") {
16932
+ const filesList = result.unusedFiles?.files || [];
16933
+ dryRunReport.files = filesList.map((f) => ({
16934
+ path: f.path,
16935
+ size: f.size
16936
+ }));
16937
+ dryRunReport.uniqueFiles = filesList.length;
16938
+ }
16939
+ if (selected === "assets") {
16940
+ const assetsList = result.publicAssets?.assets.filter((a) => !a.used) || [];
16941
+ dryRunReport.assets = assetsList.map((a) => ({
16942
+ path: a.relativePath,
16943
+ references: a.references
16944
+ }));
16945
+ dryRunReport.uniqueFiles = assetsList.length;
16946
+ }
16947
+ if (selected === "missing-assets") {
16948
+ const missingList = result.missingAssets?.assets || [];
16949
+ dryRunReport.missingAssets = missingList.map((a) => ({
16950
+ path: a.path,
16951
+ references: a.references
16952
+ }));
16953
+ dryRunReport.uniqueFiles = missingList.length;
16954
+ }
16955
+ if (selected === "broken-links") {
16956
+ const brokenList = result.brokenLinks?.links || [];
16957
+ dryRunReport.brokenLinks = brokenList.map((l) => ({
16958
+ path: l.path,
16959
+ references: l.references
16960
+ }));
16961
+ dryRunReport.uniqueFiles = brokenList.length;
16962
+ }
16721
16963
  const reportPath = join10(process.cwd(), "pruny-dry-run.json");
16722
16964
  writeFileSync3(reportPath, JSON.stringify(dryRunReport, null, 2));
16723
16965
  console.log(source_default.green(`
@@ -16726,6 +16968,25 @@ Analyzing cascading impact...`));
16726
16968
  }
16727
16969
  const selectedList = options.cleanup ? options.cleanup.split(",").map((s) => s.trim()) : [selected];
16728
16970
  let fixedSomething = false;
16971
+ if (selectedList.includes("broken-links")) {
16972
+ if (result.brokenLinks && result.brokenLinks.total > 0) {
16973
+ console.log(source_default.yellow.bold(`
16974
+ \uD83D\uDD17 Broken Internal Links Detected:`));
16975
+ console.log(source_default.gray(" (These links point to pages that don't exist. Please fix or remove them:)"));
16976
+ for (const link of result.brokenLinks.links) {
16977
+ console.log(source_default.red.bold(`
16978
+ ❌ ${link.path}`));
16979
+ for (const ref of link.references) {
16980
+ console.log(source_default.gray(` ➜ ${ref}`));
16981
+ }
16982
+ }
16983
+ console.log(source_default.yellow(`
16984
+ Create the missing pages or update the links to valid routes.`));
16985
+ } else {
16986
+ console.log(source_default.green(`
16987
+ ✅ No broken internal links found! All links are valid.`));
16988
+ }
16989
+ }
16729
16990
  if (selectedList.includes("missing-assets")) {
16730
16991
  if (result.missingAssets && result.missingAssets.total > 0) {
16731
16992
  console.log(source_default.yellow.bold(`
@@ -17108,6 +17369,14 @@ function printSummaryTable(result, context) {
17108
17369
  Unused: result.missingAssets.total
17109
17370
  });
17110
17371
  }
17372
+ if (result.brokenLinks && result.brokenLinks.total > 0) {
17373
+ summary.push({
17374
+ Category: source_default.red.bold("\uD83D\uDD17 Broken Links"),
17375
+ Total: result.brokenLinks.total,
17376
+ Used: "-",
17377
+ Unused: result.brokenLinks.total
17378
+ });
17379
+ }
17111
17380
  if (result.unusedFiles)
17112
17381
  summary.push({ Category: "Code Files (.ts/.js)", Total: result.unusedFiles.used + result.unusedFiles.unused, Used: result.unusedFiles.used, Unused: result.unusedFiles.unused });
17113
17382
  if (result.unusedExports)
@@ -17143,6 +17412,19 @@ function printSummaryTable(result, context) {
17143
17412
  console.log(source_default.yellow(`
17144
17413
  These files are referenced in code but don't exist. Update the links or create the files.`));
17145
17414
  }
17415
+ if (result.brokenLinks && result.brokenLinks.total > 0) {
17416
+ console.log(source_default.red.bold(`
17417
+ \uD83D\uDD17 Broken Internal Links:
17418
+ `));
17419
+ for (const link of result.brokenLinks.links) {
17420
+ console.log(source_default.red(` ✗ ${link.path}`));
17421
+ for (const ref of link.references) {
17422
+ console.log(source_default.dim(` → ${ref}`));
17423
+ }
17424
+ }
17425
+ console.log(source_default.yellow(`
17426
+ These links point to pages/routes that don't exist. Create the pages or fix the links.`));
17427
+ }
17146
17428
  }
17147
17429
  function printConsolidatedTable(allResults) {
17148
17430
  console.log(source_default.bold(`\uD83D\uDCCA Monorepo Summary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.36.0",
3
+ "version": "1.37.0",
4
4
  "description": "Find and remove unused Next.js API routes & Nest.js Controllers",
5
5
  "type": "module",
6
6
  "files": [