pruny 1.36.1 → 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 +263 -22
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12567,8 +12567,8 @@ import { rmSync, existsSync as existsSync9, readdirSync, lstatSync, writeFileSyn
12567
12567
  import { dirname as dirname5, join as join10, relative as relative5, resolve as resolve3 } from "node:path";
12568
12568
 
12569
12569
  // src/scanner.ts
12570
- var import_fast_glob9 = __toESM(require_out4(), 1);
12571
- 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";
12572
12572
  import { join as join7 } from "node:path";
12573
12573
 
12574
12574
  // src/patterns.ts
@@ -15729,6 +15729,176 @@ async function scanUnusedServices(config) {
15729
15729
  return { total: methods.length, methods };
15730
15730
  }
15731
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
+
15732
15902
  // src/scanner.ts
15733
15903
  function extractRoutePath(filePath) {
15734
15904
  let path2 = filePath.replace(/^src\//, "").replace(/^apps\/[^/]+\//, "").replace(/^packages\/[^/]+\//, "");
@@ -15834,19 +16004,19 @@ function extractNestMethodName(content) {
15834
16004
  return "";
15835
16005
  }
15836
16006
  function shouldIgnore(path2, ignorePatterns) {
15837
- const cleanPath = path2.replace(/\\/g, "/").replace(/^\//, "").replace(/^\.\//, "");
16007
+ const cleanPath2 = path2.replace(/\\/g, "/").replace(/^\//, "").replace(/^\.\//, "");
15838
16008
  return ignorePatterns.some((pattern) => {
15839
16009
  let cleanPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
15840
16010
  const isAbsolute4 = cleanPattern.startsWith("/");
15841
16011
  if (isAbsolute4)
15842
16012
  cleanPattern = cleanPattern.substring(1);
15843
- if (minimatch(cleanPath, cleanPattern))
16013
+ if (minimatch(cleanPath2, cleanPattern))
15844
16014
  return true;
15845
16015
  const folderPattern = cleanPattern.endsWith("/") ? cleanPattern : cleanPattern + "/";
15846
- if (cleanPath.startsWith(folderPattern))
16016
+ if (cleanPath2.startsWith(folderPattern))
15847
16017
  return true;
15848
16018
  if (!isAbsolute4 && !cleanPattern.includes("/") && !cleanPattern.includes("*")) {
15849
- if (cleanPath.endsWith("/" + cleanPattern) || cleanPath === cleanPattern)
16019
+ if (cleanPath2.endsWith("/" + cleanPattern) || cleanPath2 === cleanPattern)
15850
16020
  return true;
15851
16021
  }
15852
16022
  return false;
@@ -15863,9 +16033,9 @@ async function detectGlobalPrefix(appDir) {
15863
16033
  const mainTsAltPath = join7(appDir, "main.ts");
15864
16034
  let content;
15865
16035
  if (existsSync6(mainTsPath)) {
15866
- content = readFileSync8(mainTsPath, "utf-8");
16036
+ content = readFileSync9(mainTsPath, "utf-8");
15867
16037
  } else if (existsSync6(mainTsAltPath)) {
15868
- content = readFileSync8(mainTsAltPath, "utf-8");
16038
+ content = readFileSync9(mainTsAltPath, "utf-8");
15869
16039
  } else {
15870
16040
  return "";
15871
16041
  }
@@ -15927,7 +16097,7 @@ function getVercelCronPaths(dir) {
15927
16097
  return [];
15928
16098
  }
15929
16099
  try {
15930
- const content = readFileSync8(vercelPath, "utf-8");
16100
+ const content = readFileSync9(vercelPath, "utf-8");
15931
16101
  const config = JSON.parse(content);
15932
16102
  if (!config.crons) {
15933
16103
  return [];
@@ -15963,13 +16133,13 @@ async function scan(config) {
15963
16133
  if (prefix)
15964
16134
  detectedGlobalPrefix = prefix;
15965
16135
  }
15966
- const nextFiles = await import_fast_glob9.default(activeNextPatterns, {
16136
+ const nextFiles = await import_fast_glob10.default(activeNextPatterns, {
15967
16137
  cwd: scanCwd,
15968
16138
  ignore: config.ignore.folders
15969
16139
  });
15970
16140
  const nextRoutes = nextFiles.map((file) => {
15971
16141
  const fullPath = join7(scanCwd, file);
15972
- const content = readFileSync8(fullPath, "utf-8");
16142
+ const content = readFileSync9(fullPath, "utf-8");
15973
16143
  const { methods, methodLines } = extractExportedMethods(content);
15974
16144
  return {
15975
16145
  type: "nextjs",
@@ -15983,13 +16153,13 @@ async function scan(config) {
15983
16153
  };
15984
16154
  });
15985
16155
  const nestPatterns = ["**/*.controller.ts"];
15986
- const nestFiles = await import_fast_glob9.default(nestPatterns, {
16156
+ const nestFiles = await import_fast_glob10.default(nestPatterns, {
15987
16157
  cwd: scanCwd,
15988
16158
  ignore: config.ignore.folders
15989
16159
  });
15990
16160
  const nestRoutes = nestFiles.flatMap((file) => {
15991
16161
  const fullPath = join7(scanCwd, file);
15992
- const content = readFileSync8(fullPath, "utf-8");
16162
+ const content = readFileSync9(fullPath, "utf-8");
15993
16163
  const relativePathFromRoot = fullPath.replace(config.appSpecificScan ? config.appSpecificScan.rootDir + "/" : cwd + "/", "");
15994
16164
  return extractNestRoutes(relativePathFromRoot, content, detectedGlobalPrefix);
15995
16165
  });
@@ -16009,7 +16179,7 @@ async function scan(config) {
16009
16179
  }
16010
16180
  const referenceScanCwd = config.appSpecificScan ? config.appSpecificScan.rootDir : cwd;
16011
16181
  const extGlob = `**/*{${config.extensions.join(",")}}`;
16012
- const sourceFiles = await import_fast_glob9.default(extGlob, {
16182
+ const sourceFiles = await import_fast_glob10.default(extGlob, {
16013
16183
  cwd: referenceScanCwd,
16014
16184
  ignore: [...config.ignore.folders, ...config.ignore.files]
16015
16185
  });
@@ -16018,7 +16188,7 @@ async function scan(config) {
16018
16188
  for (const file of sourceFiles) {
16019
16189
  const filePath = join7(referenceScanCwd, file);
16020
16190
  try {
16021
- const content = readFileSync8(filePath, "utf-8");
16191
+ const content = readFileSync9(filePath, "utf-8");
16022
16192
  const refs = extractApiReferences(content);
16023
16193
  if (refs.length > 0) {
16024
16194
  fileReferences.set(file, refs);
@@ -16065,6 +16235,7 @@ async function scan(config) {
16065
16235
  routes,
16066
16236
  publicAssets,
16067
16237
  missingAssets: await scanMissingAssets(config),
16238
+ brokenLinks: await scanBrokenLinks(config),
16068
16239
  unusedFiles,
16069
16240
  unusedExports: await scanUnusedExports(config).then((result) => {
16070
16241
  const filtered = result.exports.filter((exp) => !exp.file.endsWith(".controller.ts") && !exp.file.endsWith(".controller.tsx"));
@@ -16076,8 +16247,8 @@ async function scan(config) {
16076
16247
  }
16077
16248
 
16078
16249
  // src/config.ts
16079
- var import_fast_glob10 = __toESM(require_out4(), 1);
16080
- 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";
16081
16252
  import { join as join8, resolve as resolve2, relative as relative4, dirname as dirname4 } from "node:path";
16082
16253
  var DEFAULT_CONFIG = {
16083
16254
  dir: "./",
@@ -16100,7 +16271,7 @@ var DEFAULT_CONFIG = {
16100
16271
  };
16101
16272
  function loadConfig(options) {
16102
16273
  const cwd = options.dir || "./";
16103
- 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"], {
16104
16275
  cwd,
16105
16276
  ignore: DEFAULT_CONFIG.ignore.folders,
16106
16277
  absolute: true
@@ -16126,7 +16297,7 @@ function loadConfig(options) {
16126
16297
  let excludePublic = options.excludePublic ?? false;
16127
16298
  for (const configPath of configFiles) {
16128
16299
  try {
16129
- const content = readFileSync9(configPath, "utf-8");
16300
+ const content = readFileSync10(configPath, "utf-8");
16130
16301
  const config = JSON.parse(content);
16131
16302
  const configDir = dirname4(configPath);
16132
16303
  const relDir = relative4(cwd, configDir);
@@ -16173,7 +16344,7 @@ function parseGitIgnore(dir) {
16173
16344
  if (!existsSync7(gitIgnorePath))
16174
16345
  return [];
16175
16346
  try {
16176
- const content = readFileSync9(gitIgnorePath, "utf-8");
16347
+ const content = readFileSync10(gitIgnorePath, "utf-8");
16177
16348
  return content.split(`
16178
16349
  `).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((pattern) => {
16179
16350
  if (pattern.startsWith("/") || pattern.startsWith("**/"))
@@ -16448,6 +16619,10 @@ function filterResults(result, filterPattern) {
16448
16619
  result.unusedExports.total = result.unusedExports.exports.length;
16449
16620
  result.unusedExports.unused = result.unusedExports.exports.length;
16450
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
+ }
16451
16626
  result.total = result.routes.length;
16452
16627
  result.used = result.routes.filter((r) => r.used).length;
16453
16628
  result.unused = result.routes.filter((r) => !r.used).length;
@@ -16525,6 +16700,17 @@ function printDetailedReport(result) {
16525
16700
  }
16526
16701
  console.log("");
16527
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
+ }
16528
16714
  if (!hasUnusedItems(result)) {
16529
16715
  console.log(source_default.green(`✅ Everything is used! Clean as a whistle.
16530
16716
  `));
@@ -16538,10 +16724,11 @@ function countIssues(result) {
16538
16724
  const partialRoutes = result.routes.filter((r) => r.used && r.unusedMethods.length > 0).length;
16539
16725
  const unusedAssets = result.publicAssets ? result.publicAssets.unused : 0;
16540
16726
  const missingAssets = result.missingAssets ? result.missingAssets.total : 0;
16727
+ const brokenLinks = result.brokenLinks ? result.brokenLinks.total : 0;
16541
16728
  const unusedFiles = result.unusedFiles ? result.unusedFiles.unused : 0;
16542
16729
  const unusedExports = result.unusedExports ? result.unusedExports.unused : 0;
16543
16730
  const unusedServices = result.unusedServices ? result.unusedServices.total : 0;
16544
- return unusedRoutes + partialRoutes + unusedAssets + missingAssets + unusedFiles + unusedExports + unusedServices;
16731
+ return unusedRoutes + partialRoutes + unusedAssets + missingAssets + brokenLinks + unusedFiles + unusedExports + unusedServices;
16545
16732
  }
16546
16733
  async function handleFixes(result, config, options, showBack) {
16547
16734
  const gitRoot = findGitRoot(config.dir);
@@ -16619,6 +16806,11 @@ Analyzing cascading impact...`));
16619
16806
  const title = count > 0 ? `⚠ Missing Assets (Broken Links) (${count})` : `✅ Missing Assets (0) - All good!`;
16620
16807
  choices.push({ title, value: "missing-assets" });
16621
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
+ }
16622
16814
  if (showBack) {
16623
16815
  choices.push({ title: source_default.cyan("← Back"), value: "back" });
16624
16816
  }
@@ -16688,7 +16880,8 @@ Analyzing cascading impact...`));
16688
16880
  exports: [],
16689
16881
  files: [],
16690
16882
  assets: [],
16691
- missingAssets: []
16883
+ missingAssets: [],
16884
+ brokenLinks: []
16692
16885
  };
16693
16886
  if (selected === "routes" || selected === "dry-run-json" || action === "dry-run") {
16694
16887
  dryRunReport.routes = targetRoutes.map((r) => ({
@@ -16759,6 +16952,14 @@ Analyzing cascading impact...`));
16759
16952
  }));
16760
16953
  dryRunReport.uniqueFiles = missingList.length;
16761
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
+ }
16762
16963
  const reportPath = join10(process.cwd(), "pruny-dry-run.json");
16763
16964
  writeFileSync3(reportPath, JSON.stringify(dryRunReport, null, 2));
16764
16965
  console.log(source_default.green(`
@@ -16767,6 +16968,25 @@ Analyzing cascading impact...`));
16767
16968
  }
16768
16969
  const selectedList = options.cleanup ? options.cleanup.split(",").map((s) => s.trim()) : [selected];
16769
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
+ }
16770
16990
  if (selectedList.includes("missing-assets")) {
16771
16991
  if (result.missingAssets && result.missingAssets.total > 0) {
16772
16992
  console.log(source_default.yellow.bold(`
@@ -17149,6 +17369,14 @@ function printSummaryTable(result, context) {
17149
17369
  Unused: result.missingAssets.total
17150
17370
  });
17151
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
+ }
17152
17380
  if (result.unusedFiles)
17153
17381
  summary.push({ Category: "Code Files (.ts/.js)", Total: result.unusedFiles.used + result.unusedFiles.unused, Used: result.unusedFiles.used, Unused: result.unusedFiles.unused });
17154
17382
  if (result.unusedExports)
@@ -17184,6 +17412,19 @@ function printSummaryTable(result, context) {
17184
17412
  console.log(source_default.yellow(`
17185
17413
  These files are referenced in code but don't exist. Update the links or create the files.`));
17186
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
+ }
17187
17428
  }
17188
17429
  function printConsolidatedTable(allResults) {
17189
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.1",
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": [