/ scripts / check-links.js
check-links.js
 1  #!/usr/bin/env node
 2  /**
 3   * Link checker script for documentation
 4   * Validates internal and external links
 5   */
 6  
 7  import { glob } from 'glob'
 8  import { readFile } from 'fs/promises'
 9  import path from 'path'
10  import { fileURLToPath } from 'url'
11  
12  const __dirname = path.dirname(fileURLToPath(import.meta.url))
13  const rootDir = path.join(__dirname, '..')
14  
15  const patterns = [
16    'docs/**/*.md',
17    '.vitepress/**/*.{md,ts,js}',
18    '*.md'
19  ]
20  
21  const internalLinkRegex = /\]\((\/[^)]+)\)/g
22  const externalLinkRegex = /\]\((https?:\/\/[^)]+)\)/g
23  
24  async function checkLinks() {
25    console.log('Checking documentation links...\n')
26    
27    const allFiles = await Promise.all(
28      patterns.map(pattern => glob(pattern, { cwd: rootDir, absolute: true }))
29    )
30    
31    const files = [...new Set(allFiles.flat())]
32    let totalLinks = 0
33    let errors = []
34    
35    for (const file of files) {
36      const content = await readFile(file, 'utf-8')
37      const relativePath = path.relative(rootDir, file)
38      
39      // Check internal links
40      for (const match of content.matchAll(internalLinkRegex)) {
41        totalLinks++
42        const link = match[1]
43        
44        // Skip anchors
45        const [linkPath] = link.split('#')
46        if (!linkPath) continue
47        
48        // Check if link resolves
49        const targetPath = path.join(rootDir, linkPath)
50        try {
51          await readFile(targetPath.endsWith('.md') ? targetPath : `${targetPath}.md`)
52        } catch {
53          errors.push({
54            file: relativePath,
55            link,
56            type: 'internal'
57          })
58        }
59      }
60      
61      // Check external links (lightweight - just URL format)
62      for (const match of content.matchAll(externalLinkRegex)) {
63        totalLinks++
64        const link = match[1]
65        
66        if (link.includes('example.com') || link.includes('YOUR_')) {
67          errors.push({
68            file: relativePath,
69            link,
70            type: 'placeholder'
71          })
72        }
73      }
74    }
75    
76    console.log(`Checked ${files.length} files with ${totalLinks} links\n`)
77    
78    if (errors.length === 0) {
79      console.log('✅ All links are valid!')
80      return 0
81    }
82    
83    console.log(`❌ Found ${errors.length} issues:\n`)
84    errors.forEach(({ file, link, type }) => {
85      console.log(`  [${type}] ${file}`)
86      console.log(`    → ${link}\n`)
87    })
88    
89    return 1
90  }
91  
92  checkLinks().then(code => process.exit(code))