11import { existsSync , readFileSync , statSync } from 'node:fs'
2- import path , { resolve } from 'node:path'
2+ import { extname , resolve } from 'node:path'
33import { glob } from 'tinyglobby'
44// @ts -ignore Could not find a declaration file for module 'markdown-link-extractor'.
55import markdownLinkExtractor from 'markdown-link-extractor'
66
7+ const errors : Array < {
8+ file : string
9+ link : string
10+ resolvedPath : string
11+ reason : string
12+ } > = [ ]
13+
714function isRelativeLink ( link : string ) {
815 return (
9- link &&
16+ ! link . startsWith ( '/' ) &&
1017 ! link . startsWith ( 'http://' ) &&
1118 ! link . startsWith ( 'https://' ) &&
1219 ! link . startsWith ( '//' ) &&
@@ -15,39 +22,33 @@ function isRelativeLink(link: string) {
1522 )
1623}
1724
18- function normalizePath ( p : string ) : string {
19- // Remove any trailing .md
20- p = p . replace ( `${ path . extname ( p ) } ` , '' )
21- return p
25+ /** Remove any trailing .md */
26+ function stripExtension ( p : string ) : string {
27+ return p . replace ( `${ extname ( p ) } ` , '' )
2228}
2329
24- function fileExistsForLink (
25- link : string ,
26- markdownFile : string ,
27- errors : Array < any > ,
28- ) : boolean {
30+ function relativeLinkExists ( link : string , file : string ) : boolean {
2931 // Remove hash if present
30- const filePart = link . split ( '#' ) [ 0 ]
32+ const linkWithoutHash = link . split ( '#' ) [ 0 ]
3133 // If the link is empty after removing hash, it's not a file
32- if ( ! filePart ) return false
33-
34- // Normalize the markdown file path
35- markdownFile = normalizePath ( markdownFile )
34+ if ( ! linkWithoutHash ) return false
3635
37- // Normalize the path
38- const normalizedPath = normalizePath ( filePart )
36+ // Strip the file/link extensions
37+ const filePath = stripExtension ( file )
38+ const linkPath = stripExtension ( linkWithoutHash )
3939
4040 // Resolve the path relative to the markdown file's directory
41- let absPath = resolve ( markdownFile , normalizedPath )
41+ // Nav up a level to simulate how links are resolved on the web
42+ let absPath = resolve ( filePath , '..' , linkPath )
4243
4344 // Ensure the resolved path is within /docs
4445 const docsRoot = resolve ( 'docs' )
4546 if ( ! absPath . startsWith ( docsRoot ) ) {
4647 errors . push ( {
4748 link,
48- markdownFile ,
49+ file ,
4950 resolvedPath : absPath ,
50- reason : 'navigates above /docs, invalid ' ,
51+ reason : 'Path outside /docs' ,
5152 } )
5253 return false
5354 }
@@ -76,42 +77,34 @@ function fileExistsForLink(
7677 if ( ! exists ) {
7778 errors . push ( {
7879 link,
79- markdownFile ,
80+ file ,
8081 resolvedPath : absPath ,
81- reason : 'not found' ,
82+ reason : 'Not found' ,
8283 } )
8384 }
8485 return exists
8586}
8687
87- async function findMarkdownLinks ( ) {
88+ async function verifyMarkdownLinks ( ) {
8889 // Find all markdown files in docs directory
8990 const markdownFiles = await glob ( 'docs/**/*.md' , {
9091 ignore : [ '**/node_modules/**' ] ,
9192 } )
9293
9394 console . log ( `Found ${ markdownFiles . length } markdown files\n` )
9495
95- const errors : Array < any > = [ ]
96-
9796 // Process each file
9897 for ( const file of markdownFiles ) {
9998 const content = readFileSync ( file , 'utf-8' )
100- const links : Array < any > = markdownLinkExtractor ( content )
101-
102- const filteredLinks = links . filter ( ( link : any ) => {
103- if ( typeof link === 'string' ) {
104- return isRelativeLink ( link )
105- } else if ( link && typeof link . href === 'string' ) {
106- return isRelativeLink ( link . href )
107- }
108- return false
99+ const links : Array < string > = markdownLinkExtractor ( content )
100+
101+ const relativeLinks = links . filter ( ( link : string ) => {
102+ return isRelativeLink ( link )
109103 } )
110104
111- if ( filteredLinks . length > 0 ) {
112- filteredLinks . forEach ( ( link ) => {
113- const href = typeof link === 'string' ? link : link . href
114- fileExistsForLink ( href , file , errors )
105+ if ( relativeLinks . length > 0 ) {
106+ relativeLinks . forEach ( ( link ) => {
107+ relativeLinkExists ( link , file )
115108 } )
116109 }
117110 }
@@ -120,7 +113,7 @@ async function findMarkdownLinks() {
120113 console . log ( `\n❌ Found ${ errors . length } broken links:` )
121114 errors . forEach ( ( err ) => {
122115 console . log (
123- `${ err . link } \n in : ${ err . markdownFile } \n path : ${ err . resolvedPath } \n why: ${ err . reason } \n` ,
116+ `${ err . file } \n link : ${ err . link } \n resolved : ${ err . resolvedPath } \n why: ${ err . reason } \n` ,
124117 )
125118 } )
126119 process . exit ( 1 )
@@ -129,4 +122,4 @@ async function findMarkdownLinks() {
129122 }
130123}
131124
132- findMarkdownLinks ( ) . catch ( console . error )
125+ verifyMarkdownLinks ( ) . catch ( console . error )
0 commit comments