forked from TanStack/db
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverify-links.ts
More file actions
125 lines (107 loc) · 3.32 KB
/
verify-links.ts
File metadata and controls
125 lines (107 loc) · 3.32 KB
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
import { existsSync, readFileSync, statSync } from 'node:fs'
import { extname, resolve } from 'node:path'
import { glob } from 'tinyglobby'
// @ts-ignore Could not find a declaration file for module 'markdown-link-extractor'.
import markdownLinkExtractor from 'markdown-link-extractor'
const errors: Array<{
file: string
link: string
resolvedPath: string
reason: string
}> = []
function isRelativeLink(link: string) {
return (
!link.startsWith('/') &&
!link.startsWith('http://') &&
!link.startsWith('https://') &&
!link.startsWith('//') &&
!link.startsWith('#') &&
!link.startsWith('mailto:')
)
}
/** Remove any trailing .md */
function stripExtension(p: string): string {
return p.replace(`${extname(p)}`, '')
}
function relativeLinkExists(link: string, file: string): boolean {
// Remove hash if present
const linkWithoutHash = link.split('#')[0]
// If the link is empty after removing hash, it's not a file
if (!linkWithoutHash) return false
// Strip the file/link extensions
const filePath = stripExtension(file)
const linkPath = stripExtension(linkWithoutHash)
// Resolve the path relative to the markdown file's directory
// Nav up a level to simulate how links are resolved on the web
let absPath = resolve(filePath, '..', linkPath)
// Ensure the resolved path is within /docs
const docsRoot = resolve('docs')
if (!absPath.startsWith(docsRoot)) {
errors.push({
link,
file,
resolvedPath: absPath,
reason: 'Path outside /docs',
})
return false
}
// Check if this is an example path
const isExample = absPath.includes('/examples/')
let exists = false
if (isExample) {
// Transform /docs/framework/{framework}/examples/ to /examples/{framework}/
absPath = absPath.replace(
/\/docs\/framework\/([^/]+)\/examples\//,
'/examples/$1/',
)
// For examples, we want to check if the directory exists
exists = existsSync(absPath) && statSync(absPath).isDirectory()
} else {
// For non-examples, we want to check if the .md file exists
if (!absPath.endsWith('.md')) {
absPath = `${absPath}.md`
}
exists = existsSync(absPath)
}
if (!exists) {
errors.push({
link,
file,
resolvedPath: absPath,
reason: 'Not found',
})
}
return exists
}
async function verifyMarkdownLinks() {
// Find all markdown files in docs directory
const markdownFiles = await glob('docs/**/*.md', {
ignore: ['**/node_modules/**'],
})
console.log(`Found ${markdownFiles.length} markdown files\n`)
// Process each file
for (const file of markdownFiles) {
const content = readFileSync(file, 'utf-8')
const links: Array<string> = markdownLinkExtractor(content)
const relativeLinks = links.filter((link: string) => {
return isRelativeLink(link)
})
if (relativeLinks.length > 0) {
relativeLinks.forEach((link) => {
relativeLinkExists(link, file)
})
}
}
if (errors.length > 0) {
console.log(`\n❌ Found ${errors.length} broken links:`)
errors.forEach((err) => {
console.log(
`${err.file}\n link: ${err.link}\n resolved: ${err.resolvedPath}\n why: ${err.reason}\n`,
)
})
process.exit(1)
} else {
console.log('\n✅ No broken links found!')
}
}
verifyMarkdownLinks().catch(console.error)