From ea37ba6d8c64671894ce58ab48d9284068befb0d Mon Sep 17 00:00:00 2001 From: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:37:07 +1000 Subject: [PATCH 1/2] ci: sync github config --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 +- .github/ISSUE_TEMPLATE/config.yml | 9 +- .github/pull_request_template.md | 9 ++ .github/workflows/pr.yml | 2 + .nvmrc | 2 +- package.json | 5 +- pnpm-lock.yaml | 106 +++++++++++++++++++++ scripts/verify-links.ts | 132 ++++++++++++++++++++++++++ 8 files changed, 262 insertions(+), 7 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 scripts/verify-links.ts diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index db8a6f470..d8f8d4b31 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,4 +1,4 @@ -name: 'šŸ› Bug report' +name: šŸ› Bug Report description: Report a reproducible bug or regression body: - type: markdown @@ -108,7 +108,7 @@ body: description: | If you are using TypeScript, please let us know the exact version of TypeScript you were using when the issue occurred. placeholder: | - e.g. v4.5.4 + e.g. v5.2.2 - type: textarea id: additional attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ce23f2f8c..4ec47ee22 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,11 @@ blank_issues_enabled: false contact_links: - - name: Feature Requests & Questions - url: https://github.com/tanstack/virtual/discussions + - name: šŸ¤” Feature Requests & Questions + url: https://github.com/TanStack/virtual/discussions about: Please ask and answer questions here. - - name: Community Chat + - name: šŸ’¬ Community Chat url: https://discord.gg/mQd7egN about: A dedicated discord server hosted by TanStack + - name: šŸ¦‹ TanStack Bluesky + url: https://bsky.app/profile/tanstack.com + about: Stay up to date with new releases of our libraries diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..285efb166 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ +## šŸŽÆ Changes + + + +## āœ… Checklist + +- [ ] I have followed the steps listed in the [Contributing guide](https://github.com/TanStack/config/blob/main/CONTRIBUTING.md). +- [ ] I have tested and linted this code locally. +- [ ] I have generated a [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md) for this PR, or this PR should not release a new version. diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9b72a44ba..e4a052013 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -32,6 +32,8 @@ 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/.nvmrc b/.nvmrc index d5b283a3a..9d11232a6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.13.1 +24.4.1 diff --git a/package.json b/package.json index 370508b6c..412d5bea0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "https://github.com/TanStack/virtual.git" }, - "packageManager": "pnpm@10.12.1", + "packageManager": "pnpm@10.13.1", "type": "module", "scripts": { "clean": "pnpm --filter \"./packages/**\" run clean", @@ -28,6 +28,7 @@ "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" @@ -48,12 +49,14 @@ "eslint": "^9.29.0", "jsdom": "^25.0.1", "knip": "^5.61.0", + "markdown-link-extractor": "^4.0.2", "nx": "^20.8.2", "premove": "^4.0.0", "prettier": "^3.5.3", "prettier-plugin-svelte": "^3.4.0", "publint": "^0.3.12", "sherif": "^1.5.0", + "tinyglobby": "^0.2.14", "typescript": "5.2.2", "vite": "^5.4.19", "vitest": "^2.1.9" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fe1838cc..51b482e63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: knip: specifier: ^5.61.0 version: 5.61.3(@types/node@22.15.34)(typescript@5.2.2) + markdown-link-extractor: + specifier: ^4.0.2 + version: 4.0.2 nx: specifier: ^20.8.2 version: 20.8.2 @@ -53,6 +56,9 @@ importers: sherif: specifier: ^1.5.0 version: 1.6.1 + tinyglobby: + specifier: ^0.2.14 + version: 0.2.14 typescript: specifier: 5.2.2 version: 5.2.2 @@ -4444,6 +4450,13 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.1.2: + resolution: {integrity: sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==} + engines: {node: '>=20.18.1'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -4866,6 +4879,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} @@ -5390,6 +5406,12 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-link-extractor@1.0.5: + resolution: {integrity: sha512-ADd49pudM157uWHwHQPUSX4ssMsvR/yHIswOR5CUfBdK9g9ZYGMhVSE6KZVHJ6kCkR0gH4htsfzU6zECDNVwyw==} + + htmlparser2@10.0.0: + resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} @@ -6010,6 +6032,14 @@ packages: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true + markdown-link-extractor@4.0.2: + resolution: {integrity: sha512-5cUOu4Vwx1wenJgxaudsJ8xwLUMN7747yDJX3V/L7+gi3e4MsCm7w5nbrDQQy8nEfnl4r5NV3pDXMAjhGXYXAw==} + + marked@12.0.2: + resolution: {integrity: sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==} + engines: {node: '>= 18'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -6470,6 +6500,12 @@ packages: parse5-html-rewriting-stream@7.0.0: resolution: {integrity: sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==} + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + parse5-sax-parser@7.0.0: resolution: {integrity: sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==} @@ -7393,6 +7429,10 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + tinypool@1.1.1: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -7541,6 +7581,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.12.0: + resolution: {integrity: sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==} + engines: {node: '>=20.18.1'} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -11418,6 +11462,29 @@ snapshots: check-error@2.1.1: {} + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.1.2: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.0.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.12.0 + whatwg-mimetype: 4.0.0 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -11793,6 +11860,11 @@ snapshots: encodeurl@2.0.0: {} + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + encoding@0.1.13: dependencies: iconv-lite: 0.6.3 @@ -12441,6 +12513,17 @@ snapshots: html-escaper@2.0.2: {} + html-link-extractor@1.0.5: + dependencies: + cheerio: 1.1.2 + + htmlparser2@10.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 6.0.1 + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 @@ -13130,6 +13213,13 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + markdown-link-extractor@4.0.2: + dependencies: + html-link-extractor: 1.0.5 + marked: 12.0.2 + + marked@12.0.2: {} + math-intrinsics@1.1.0: {} mdn-data@2.0.30: {} @@ -13666,6 +13756,15 @@ snapshots: parse5: 7.3.0 parse5-sax-parser: 7.0.0 + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + parse5-sax-parser@7.0.0: dependencies: parse5: 7.3.0 @@ -14610,6 +14709,11 @@ snapshots: tinyexec@0.3.2: {} + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + tinypool@1.1.1: {} tinyrainbow@1.2.0: {} @@ -14730,6 +14834,8 @@ snapshots: undici-types@6.21.0: {} + undici@7.12.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: diff --git a/scripts/verify-links.ts b/scripts/verify-links.ts new file mode 100644 index 000000000..268a0ac90 --- /dev/null +++ b/scripts/verify-links.ts @@ -0,0 +1,132 @@ +import { existsSync, readFileSync, statSync } from 'node:fs' +import path, { 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' + +function isRelativeLink(link: string) { + return ( + link && + !link.startsWith('http://') && + !link.startsWith('https://') && + !link.startsWith('//') && + !link.startsWith('#') && + !link.startsWith('mailto:') + ) +} + +function normalizePath(p: string): string { + // Remove any trailing .md + p = p.replace(`${path.extname(p)}`, '') + return p +} + +function fileExistsForLink( + link: string, + markdownFile: string, + errors: Array, +): boolean { + // Remove hash if present + const filePart = 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) + + // Normalize the path + const normalizedPath = normalizePath(filePart) + + // Resolve the path relative to the markdown file's directory + let absPath = resolve(markdownFile, normalizedPath) + + // Ensure the resolved path is within /docs + const docsRoot = resolve('docs') + if (!absPath.startsWith(docsRoot)) { + errors.push({ + link, + markdownFile, + resolvedPath: absPath, + reason: 'navigates above /docs, invalid', + }) + 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, + markdownFile, + resolvedPath: absPath, + reason: 'not found', + }) + } + return exists +} + +async function findMarkdownLinks() { + // 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 = [] + + // 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 + }) + + if (filteredLinks.length > 0) { + filteredLinks.forEach((link) => { + const href = typeof link === 'string' ? link : link.href + fileExistsForLink(href, file, errors) + }) + } + } + + if (errors.length > 0) { + 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`, + ) + }) + process.exit(1) + } else { + console.log('\nāœ… No broken links found!') + } +} + +findMarkdownLinks().catch(console.error) From c41e33132be492cca8d8a0b925a0f97a3de55a93 Mon Sep 17 00:00:00 2001 From: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:47:23 +1000 Subject: [PATCH 2/2] Fix contributing guide --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3fd8996ad..532efc03b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,7 +57,7 @@ If you have been assigned to fix an issue or develop a new feature, please follo The documentations for all the TanStack projects are hosted on [tanstack.com](https://tanstack.com), which is a TanStack Start application (https://github.com/TanStack/tanstack.com). You need to run this app locally to preview your changes in the `TanStack/virtual` docs. > [!NOTE] -> The website fetches the doc pages from GitHub in production, and searches for them at `../config/docs` in development. Your local clone of `TanStack/virtual` needs to be in the same directory as the local clone of `TansStack/tanstack.com`. +> The website fetches the doc pages from GitHub in production, and searches for them at `../virtual/docs` in development. Your local clone of `TanStack/virtual` needs to be in the same directory as the local clone of `TanStack/tanstack.com`. You can follow these steps to set up the docs for local development: @@ -103,7 +103,7 @@ pnpm dev 4. Now you can visit http://localhost:3000/virtual/latest/docs/overview in the browser and see the changes you make in `TanStack/virtual/docs` there. > [!WARNING] -> You will need to update the `docs/config.json` file (in `TanStack/config`) if you add a new documentation page! +> You will need to update the `docs/config.json` file (in `TanStack/virtual`) if you add a new documentation page! You can see the whole process in the screen capture below: