@@ -7,6 +7,20 @@ import {
77import type { InferPageType } from "fumadocs-core/source" ;
88import { source } from "@/lib/source" ;
99import { 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
1125async 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+
139265void checkLinks ( ) ;
0 commit comments