Skip to content

Commit e9616e7

Browse files
committed
chore: adjusted linked validation script to account for navigation trees
1 parent 524a176 commit e9616e7

1 file changed

Lines changed: 127 additions & 1 deletion

File tree

scripts/link-validation.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ import {
77
import type { InferPageType } from "fumadocs-core/source";
88
import { source } from "@/lib/source";
99
import { writeFileSync } from "fs";
10+
import {
11+
arbitrumStylusTree,
12+
ethereumEvmTree,
13+
impactTree,
14+
midnightTree,
15+
polkadotTree,
16+
starknetTree,
17+
stellarTree,
18+
uniswapTree,
19+
zamaTree,
20+
type NavigationNode,
21+
type NavigationPage,
22+
type NavigationFolder,
23+
} from "@/navigation";
1024

1125
async function checkLinks() {
1226
// Parse command line arguments
@@ -52,6 +66,11 @@ async function checkLinks() {
5266
checkRelativePaths: "as-url",
5367
});
5468

69+
// Validate navigation URLs if requested
70+
let navigationErrors: Array<{ tree: string; url: string; reason: string }> =
71+
[];
72+
navigationErrors = await validateNavigationUrls(scanned);
73+
5574
if (outputFile) {
5675
// Generate custom output format for file
5776
let output = "";
@@ -70,16 +89,41 @@ async function checkLinks() {
7089
}
7190
}
7291

73-
output += `\nSummary: ${totalErrors} errors found in ${validationResults.filter((r) => r.errors.length > 0).length} files out of ${totalFiles} total files\n`;
92+
// Add navigation errors to output
93+
if (navigationErrors.length > 0) {
94+
output += `\nInvalid URLs in Navigation Trees:\n`;
95+
for (const error of navigationErrors) {
96+
totalErrors++;
97+
output += `${error.tree}: ${error.url} - ${error.reason}\n`;
98+
}
99+
output += "------\n";
100+
}
101+
102+
output += `\nSummary: ${totalErrors} errors found in ${validationResults.filter((r) => r.errors.length > 0).length} files out of ${totalFiles} total files`;
103+
if (navigationErrors.length > 0) {
104+
output += ` + ${navigationErrors.length} navigation errors`;
105+
}
106+
output += `\n`;
74107

75108
writeFileSync(outputFile, output);
76109
console.log(`Results saved to ${outputFile}`);
77110
console.log(
78111
`${totalErrors} errors found in ${validationResults.filter((r) => r.errors.length > 0).length} files`,
79112
);
113+
if (navigationErrors.length > 0) {
114+
console.log(`${navigationErrors.length} navigation errors found`);
115+
}
80116
} else {
81117
// Use default printErrors for console output
82118
printErrors(validationResults, true);
119+
120+
// Print navigation errors
121+
if (navigationErrors.length > 0) {
122+
console.log("\n❌ Navigation URL Errors:");
123+
for (const error of navigationErrors) {
124+
console.log(` ${error.tree}: ${error.url} - ${error.reason}`);
125+
}
126+
}
83127
}
84128
}
85129

@@ -136,4 +180,86 @@ function getFiles(scope?: string | null) {
136180
return Promise.all(promises);
137181
}
138182

183+
function extractUrlsFromNavigation(
184+
nodes: NavigationNode[],
185+
urls: string[] = [],
186+
): string[] {
187+
for (const node of nodes) {
188+
if (node.type === "page") {
189+
const page = node as NavigationPage;
190+
// Only validate internal URLs (not external links)
191+
if (!page.external && page.url) {
192+
urls.push(page.url);
193+
}
194+
} else if (node.type === "folder") {
195+
const folder = node as NavigationFolder;
196+
if (folder.index && !folder.index.external) {
197+
urls.push(folder.index.url);
198+
}
199+
if (folder.children) {
200+
extractUrlsFromNavigation(folder.children, urls);
201+
}
202+
}
203+
}
204+
return urls;
205+
}
206+
207+
async function validateNavigationUrls(
208+
scanned: Awaited<ReturnType<typeof scanURLs>>,
209+
): Promise<Array<{ tree: string; url: string; reason: string }>> {
210+
const navigationTrees = {
211+
"Ethereum & EVM": ethereumEvmTree,
212+
"Arbitrum Stylus": arbitrumStylusTree,
213+
Stellar: stellarTree,
214+
Midnight: midnightTree,
215+
Starknet: starknetTree,
216+
"Zama FHEVM": zamaTree,
217+
"Uniswap Hooks": uniswapTree,
218+
Polkadot: polkadotTree,
219+
"OpenZeppelin Impact": impactTree,
220+
};
221+
222+
const errors: Array<{ tree: string; url: string; reason: string }> = [];
223+
224+
for (const [treeName, tree] of Object.entries(navigationTrees)) {
225+
const urls = extractUrlsFromNavigation(tree.children);
226+
227+
for (const url of urls) {
228+
// Split URL into path and fragment
229+
const [urlPath, fragment] = url.split("#");
230+
231+
// Check if the URL path exists in the scanned pages
232+
// scanned.urls is a Map<string, UrlMeta>
233+
const found = scanned.urls.has(urlPath);
234+
235+
if (!found) {
236+
// Check fallback URLs (dynamic routes)
237+
const foundInFallback = scanned.fallbackUrls.some((fallback) =>
238+
fallback.url.test(urlPath),
239+
);
240+
241+
if (!foundInFallback) {
242+
errors.push({
243+
tree: treeName,
244+
url,
245+
reason: "URL not found in site pages",
246+
});
247+
}
248+
} else if (fragment) {
249+
// If URL has a fragment, validate that the fragment exists on the page
250+
const urlMeta = scanned.urls.get(urlPath);
251+
if (urlMeta?.hashes && !urlMeta.hashes.includes(fragment)) {
252+
errors.push({
253+
tree: treeName,
254+
url,
255+
reason: `Fragment '#${fragment}' not found on page`,
256+
});
257+
}
258+
}
259+
}
260+
}
261+
262+
return errors;
263+
}
264+
139265
void checkLinks();

0 commit comments

Comments
 (0)