Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ The following tools are required to use these packages:

## Utilities

- [ESLint](../eslint.md)
- [Publish](../publish.md)
- [Vite](../vite.md)
- [ESLint](./eslint.md)
- [Publish](./publish.md)
- [Vite](./vite.md)

## Conventions

- [CI/CD](../ci-cd.md)
- [Dependencies](../dependencies.md)
- [Package Structure](../package-structure.md)
- [CI/CD](./ci-cd.md)
- [Dependencies](./dependencies.md)
- [Package Structure](./package-structure.md)
2 changes: 1 addition & 1 deletion docs/package-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The following structure ensures packages work optimally with our monorepo/Nx wor

### `./vite.config.ts`

- Includes config for Vitest, and for Vite if [@tanstack/vite-config](../vite.md) is used
- Includes config for Vitest, and for Vite if [@tanstack/vite-config](./vite.md) is used

### `./src`

Expand Down
85 changes: 44 additions & 41 deletions scripts/verify-links.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { existsSync, readFileSync, statSync } from 'node:fs'
import path, { resolve } from 'node:path'
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 &&
!link.startsWith(`/`) &&
!link.startsWith('http://') &&
!link.startsWith('https://') &&
!link.startsWith('//') &&
Expand All @@ -15,39 +22,33 @@ function isRelativeLink(link: string) {
)
}

function normalizePath(p: string): string {
// Remove any trailing .md
p = p.replace(`${path.extname(p)}`, '')
return p
/** Remove any trailing .md */
function stripExtension(p: string): string {
return p.replace(`${extname(p)}`, '')
}

function fileExistsForLink(
link: string,
markdownFile: string,
errors: Array<any>,
): boolean {
function relativeLinkExists(link: string, file: string): boolean {
// Remove hash if present
const filePart = link.split('#')[0]
const linkWithoutHash = link.split('#')[0]
// If the link is empty after removing hash, it's not a file
if (!filePart) return false

// Normalize the markdown file path
markdownFile = normalizePath(markdownFile)
if (!linkWithoutHash) return false

// Normalize the path
const normalizedPath = normalizePath(filePart)
// Strip the file/link extensions
const filePath = stripExtension(file)
const linkPath = stripExtension(linkWithoutHash)

// Resolve the path relative to the markdown file's directory
let absPath = resolve(markdownFile, normalizedPath)
// 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,
markdownFile,
file,
resolvedPath: absPath,
reason: 'navigates above /docs, invalid',
reason: 'Path outside /docs',
})
return false
}
Expand Down Expand Up @@ -76,42 +77,44 @@ function fileExistsForLink(
if (!exists) {
errors.push({
link,
markdownFile,
file,
resolvedPath: absPath,
reason: 'not found',
reason: 'Not found',
})
}
return exists
}

async function findMarkdownLinks() {
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`)

const errors: Array<any> = []

// Process each file
for (const file of markdownFiles) {
const content = readFileSync(file, 'utf-8')
const links: Array<any> = markdownLinkExtractor(content)

const filteredLinks = links.filter((link: any) => {
if (typeof link === 'string') {
return isRelativeLink(link)
} else if (link && typeof link.href === 'string') {
return isRelativeLink(link.href)
}
return false
const links: Array<string> = markdownLinkExtractor(content)

const relativeLinks = links.filter((link: string) => {
return isRelativeLink(link)
})

if (filteredLinks.length > 0) {
filteredLinks.forEach((link) => {
const href = typeof link === 'string' ? link : link.href
fileExistsForLink(href, file, errors)
if (relativeLinks.length > 0) {
relativeLinks.forEach((link) => {
if (!link.startsWith('.')) {
errors.push({
link,
file,
resolvedPath: '',
reason: 'Does not start with ./ or ../',
})
return
}

relativeLinkExists(link, file)
})
}
}
Expand All @@ -120,7 +123,7 @@ async function findMarkdownLinks() {
console.log(`\n❌ Found ${errors.length} broken links:`)
errors.forEach((err) => {
console.log(
`${err.link}\n in: ${err.markdownFile}\n path: ${err.resolvedPath}\n why: ${err.reason}\n`,
`${err.file}\n link: ${err.link}\n resolved: ${err.resolvedPath}\n why: ${err.reason}\n`,
)
})
process.exit(1)
Expand All @@ -129,4 +132,4 @@ async function findMarkdownLinks() {
}
}

findMarkdownLinks().catch(console.error)
verifyMarkdownLinks().catch(console.error)
Loading