diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index dd4b6c3e..7223d30e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -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 diff --git a/nx.json b/nx.json index 5a1255a7..9e704626 100644 --- a/nx.json +++ b/nx.json @@ -22,6 +22,10 @@ ] }, "targetDefaults": { + "test:docs": { + "cache": true, + "inputs": ["{workspaceRoot}/docs/**/*"] + }, "test:knip": { "cache": true, "inputs": ["{workspaceRoot}/**/*"] diff --git a/package.json b/package.json index 64cdfb8a..3bfe20b0 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" ] diff --git a/scripts/verify-links.ts b/scripts/verify-links.ts index 268a0ac9..8a1c40dd 100644 --- a/scripts/verify-links.ts +++ b/scripts/verify-links.ts @@ -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('//') && @@ -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, -): 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 } @@ -76,15 +77,15 @@ 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/**'], @@ -92,26 +93,18 @@ async function findMarkdownLinks() { console.log(`Found ${markdownFiles.length} markdown files\n`) - const errors: Array = [] - // Process each file for (const file of markdownFiles) { const content = readFileSync(file, 'utf-8') - const links: Array = 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 = 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) }) } } @@ -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) @@ -129,4 +122,4 @@ async function findMarkdownLinks() { } } -findMarkdownLinks().catch(console.error) +verifyMarkdownLinks().catch(console.error) diff --git a/tsconfig.json b/tsconfig.json index fcc60281..765ad9cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,5 @@ "strict": true, "target": "ES2020" }, - "include": ["eslint.config.js", "prettier.config.js", "scripts"] + "include": ["*.config.*", "scripts"] }