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
2 changes: 0 additions & 2 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ jobs:
run: pnpm exec playwright install chromium
- name: Run Checks
run: pnpm run test:pr
- name: Verify Links
run: pnpm run verify-links
preview:
name: Preview
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
]
},
"targetDefaults": {
"test:docs": {
"cache": true,
"inputs": ["{workspaceRoot}/docs/**/*"]
},
"test:knip": {
"cache": true,
"inputs": ["{workspaceRoot}/**/*"]
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@
"type": "git",
"url": "https://github.com/TanStack/virtual.git"
},
"packageManager": "pnpm@10.17.0",
"packageManager": "pnpm@10.24.0",
"type": "module",
"scripts": {
"clean": "pnpm --filter \"./packages/**\" run clean",
"preinstall": "node -e \"if(process.env.CI == 'true') {console.log('Skipping preinstall...'); process.exit(1)}\" || npx -y only-allow pnpm",
"test": "pnpm run test:ci",
"test:pr": "nx affected --targets=test:sherif,test:knip,test:eslint,test:lib,test:e2e,test:types,test:build,build",
"test:ci": "nx run-many --targets=test:sherif,test:knip,test:eslint,test:lib,test:e2e,test:types,test:build,build",
"test:pr": "nx affected --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:e2e,test:types,test:build,build",
"test:ci": "nx run-many --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:e2e,test:types,test:build,build",
"test:eslint": "nx affected --target=test:eslint",
"test:format": "pnpm run prettier --check",
"test:sherif": "sherif",
Expand All @@ -22,19 +21,20 @@
"test:types": "nx affected --target=test:types --exclude=examples/**",
"test:e2e": "nx affected --target=test:e2e --exclude=examples/**",
"test:knip": "knip",
"test:docs": "node scripts/verify-links.ts",
"build": "nx affected --target=build --exclude=examples/**",
"build:all": "nx run-many --target=build --exclude=examples/**",
"watch": "pnpm run build:all && nx watch --all -- pnpm run build:all",
"dev": "pnpm run watch",
"prettier": "prettier --ignore-unknown '**/*'",
"prettier:write": "pnpm run prettier --write",
"verify-links": "node scripts/verify-links.ts",
"changeset": "changeset",
"changeset:version": "changeset version && pnpm install --no-frozen-lockfile && pnpm prettier:write",
"changeset:publish": "changeset publish"
},
"nx": {
"includedScripts": [
"test:docs",
"test:knip",
"test:sherif"
]
Expand Down
75 changes: 34 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,34 @@ 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) => {
relativeLinkExists(link, file)
})
}
}
Expand All @@ -120,7 +113,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 +122,4 @@ async function findMarkdownLinks() {
}
}

findMarkdownLinks().catch(console.error)
verifyMarkdownLinks().catch(console.error)
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@
"strict": true,
"target": "ES2020"
},
"include": ["eslint.config.js", "prettier.config.js", "scripts"]
"include": ["*.config.*", "scripts"]
}
Loading