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.
- package/dist/index.js +263 -22
- 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
|
|
12571
|
-
import { existsSync as existsSync6, readFileSync as
|
|
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
|
|
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(
|
|
16013
|
+
if (minimatch(cleanPath2, cleanPattern))
|
|
15844
16014
|
return true;
|
|
15845
16015
|
const folderPattern = cleanPattern.endsWith("/") ? cleanPattern : cleanPattern + "/";
|
|
15846
|
-
if (
|
|
16016
|
+
if (cleanPath2.startsWith(folderPattern))
|
|
15847
16017
|
return true;
|
|
15848
16018
|
if (!isAbsolute4 && !cleanPattern.includes("/") && !cleanPattern.includes("*")) {
|
|
15849
|
-
if (
|
|
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 =
|
|
16036
|
+
content = readFileSync9(mainTsPath, "utf-8");
|
|
15867
16037
|
} else if (existsSync6(mainTsAltPath)) {
|
|
15868
|
-
content =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
16080
|
-
import { existsSync as existsSync7, readFileSync as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|