SIGN IN SIGN UP
TanStack / query UNCLAIMED

🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.

48947 0 0 TypeScript
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
2025-05-24 06:48:59 +00:00
if (isExample) {
// Transform /docs/framework/{framework}/examples/ to /examples/{framework}/
2025-05-24 06:48:59 +00:00
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:`)
2025-05-24 06:48:59 +00:00
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)