-
Notifications
You must be signed in to change notification settings - Fork 0
/
check-links.js
146 lines (124 loc) · 4.34 KB
/
check-links.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
const fs = require('fs')
const path = require('path')
let brokenRelativeLinksCount = 0
let brokenAbsoluteLinksCount = 0
let brokenExternalLinksCount = 0
let brokenFragmentLinksCount = 0
let linksChecked = 0
/** Build a list recursively of all the mdx files */
const getFilesInDirectory = (dir) => {
let results = []
let list = fs.readdirSync(dir)
list.forEach((file) => {
file = path.join(dir, file)
let stat = fs.statSync(file)
if (stat?.isDirectory()) {
results = results.concat(getFilesInDirectory(file))
} else {
if (path.extname(file) === '.mdx') {
results.push(file)
}
}
})
return results
}
const checkFileExists = (filePath) => {
// if filePath starts with /images, prefix with publicDirectory
if (filePath.startsWith('/images')) filePath = path.join('./public', filePath)
return fs.existsSync(filePath)
}
const externalLinkCache = {}
const checkExternalLink = async (url, fileName) => {
// Skip these for deployed builds, too unreliable
if (process.env.VERCEL) return
// No need to check the same link multiple times
if (externalLinkCache[url]) return
externalLinkCache[url] = true
function fail(statuscode) {
console.log(
`❌🌐 Broken link (${
statuscode || 'Unknown status code'
}) in ${fileName} -> ${url}`
)
brokenExternalLinksCount++
}
try {
// console.log(`Checking external link: ${url}`)
const response = await fetch(url)
if (!response.ok) fail(response.status)
} catch (err) {
fail()
}
}
const checkUrlFragment = (filePath, fragment, originalLink, file) => {
const fileContent = fs.readFileSync(filePath, 'utf-8')
const headers = fileContent.match(/^#+\s+.+$/gm)
const formattedFragment = fragment
.toLowerCase()
.replace(/[^a-z0-9\- ]+/g, '') // remove non alphanumeric, non-hyphen, non-space characters
.replace(/[\s]+/g, '-') // replace spaces with hyphen
const exists = headers.some((header) => {
const formattedHeader = header
.replace(/^#+\s+/g, '') // remove the leading hashes and space
.toLowerCase()
.replace(/[\s]+/g, '-') // replace spaces with hyphen
.replace(/[^a-z0-9\- ]+/g, '') // remove non alphanumeric, non-hyphen, non-space characters
return formattedHeader === formattedFragment
})
if (!exists) {
console.log(`❌⚓ broken fragment in ${file} -> ${originalLink}`)
brokenFragmentLinksCount++
}
}
const runCheck = async () => {
const startDirectory = './pages'
const files = getFilesInDirectory(startDirectory)
console.log('Found ' + files.length + ' mdx files to review...\n')
const linkRegex = /\[.*?\]\((<.*?>|.*?)\)/g
await Promise.all(
files.map(async (file) => {
const fileContent = fs.readFileSync(file, 'utf-8')
let match
while ((match = linkRegex.exec(fileContent)) !== null) {
const link = match[1].replace(/<|>/g, '') // markdown links may have angle brackets in them (to disambiguate multiple parens)
linksChecked++
const [urlWithoutFragment, fragment] = link.split('#')
// Skip mailto links
if (urlWithoutFragment.startsWith('mailto:')) continue
// Is it an external link?
if (
urlWithoutFragment.startsWith('http://') ||
urlWithoutFragment.startsWith('https://')
) {
await checkExternalLink(urlWithoutFragment, file)
continue
}
const filePath = path.join(startDirectory, urlWithoutFragment) + '.mdx'
const exists = checkFileExists(filePath)
if (!exists) {
console.log(`❌ broken inline link in ${file} -> ${link}`)
if (link.startsWith('/')) brokenAbsoluteLinksCount++
else brokenRelativeLinksCount++
} else if (fragment) {
checkUrlFragment(filePath, fragment, link, file)
}
}
})
)
// Summarize results
const total =
brokenRelativeLinksCount +
brokenAbsoluteLinksCount +
brokenExternalLinksCount +
brokenFragmentLinksCount
if (!total) {
console.log(`✅ Links checked: ${linksChecked}.`)
process.exit()
} else {
console.log(
`🟦 Links checked: ${linksChecked}.\n\n Broken links: ${total}, relative: ${brokenRelativeLinksCount}, absolute: ${brokenAbsoluteLinksCount}, external: ${brokenExternalLinksCount}, fragment: ${brokenFragmentLinksCount}`
)
process.exit(1)
}
}
runCheck()