From 0e39728a8b0dbe0714101baf6c96475e8103c0c8 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 31 Dec 2024 18:06:34 -0500 Subject: [PATCH 01/15] Added a script to check the validity of docs links and a .github action --- .github/workflows/check-links.yml | 27 +++++ docs/nav.js | 18 ++- nav.ts | 1 + package.json | 5 +- scripts/checkLinks.ts | 124 ++++++++++++++++++++ tsconfig.json | 2 + yarn.lock | 189 ++++++++++++++++++++++++++++++ 7 files changed, 362 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/check-links.yml create mode 100644 scripts/checkLinks.ts diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 00000000..ffdc10df --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,27 @@ +name: Check Link Validity in Documentation + +on: + pull_request: + paths: + - '**/*.md' + - 'nav.ts' + +jobs: + check-links: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16' # or the version of Node.js you're using + + - name: Install dependencies + run: | + npm install + + - name: Run link check + run: | + npm run check-links diff --git a/docs/nav.js b/docs/nav.js index a43c2e29..8d368615 100644 --- a/docs/nav.js +++ b/docs/nav.js @@ -1,16 +1,28 @@ "use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; Object.defineProperty(exports, "__esModule", { value: true }); function page(title, slug, path, props) { - return { type: 'page', path, slug, title, ...props }; + return __assign({ type: 'page', path: path, slug: slug, title: title }, props); } function section(title) { - return { type: 'section', title }; + return { type: 'section', title: title }; } -const nav = { +var nav = { items: [ section('Intro'), page('Overview', 'index', 'index.md'), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? page('Getting Started', 'getting-started', 'getting-started.md'), + page('asdfasdfStarted', 'getting-started', 'getting-started.md'), section('Deploying'), page('Testnet', 'deploying/testnet', 'deploying/testnet.md'), section('Migration Guides'), diff --git a/nav.ts b/nav.ts index 8ca41be7..c4ae2232 100644 --- a/nav.ts +++ b/nav.ts @@ -32,6 +32,7 @@ const nav: Nav = { section('Intro'), page('Overview', 'index', 'index.md'), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? page('Getting Started', 'getting-started', 'getting-started.md'), + page('asdfasdfStarted', 'getting-started', 'getting-started.md'), section('Deploying'), page('Testnet', 'deploying/testnet', 'deploying/testnet.md'), diff --git a/package.json b/package.json index 2c2b9445..e7716fa4 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,13 @@ "main": "index.js", "dependencies": {}, "devDependencies": { + "@types/node": "^22.10.2", + "tsx": "^4.19.2", "typescript": "^5.3.2" }, "scripts": { - "build": "tsc" + "build": "tsc nav.ts --outDir docs", + "check-links": "tsx scripts/checkLinks.ts" }, "author": "Clockwork Labs", "license": "ISC" diff --git a/scripts/checkLinks.ts b/scripts/checkLinks.ts new file mode 100644 index 00000000..2813e58c --- /dev/null +++ b/scripts/checkLinks.ts @@ -0,0 +1,124 @@ +import fs from 'fs'; +import path from 'path'; +import nav from '../nav'; // Import the nav object directly + +// Function to extract slugs from the nav object and prefix them with /docs +function extractSlugsFromNav(nav: { items: any[] }): string[] { + const slugs: string[] = []; + + function traverseNav(items: any[]): void { + items.forEach((item) => { + if (item.type === 'page' && item.slug) { + slugs.push(`/docs/${item.slug}`); // Prefix slugs with /docs + } else if (item.type === 'section' && item.items) { + traverseNav(item.items); // Recursively traverse sections + } + }); + } + + traverseNav(nav.items); + return slugs; +} + +// Function to extract links from markdown files with line numbers +function extractLinksFromMarkdown(filePath: string): { link: string; line: number }[] { + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const lines = fileContent.split('\n'); + const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; + + const links: { link: string; line: number }[] = []; + lines.forEach((lineContent, index) => { + let match: RegExpExecArray | null; + while ((match = linkRegex.exec(lineContent)) !== null) { + links.push({ link: match[2], line: index + 1 }); // Add 1 to make line numbers 1-based + } + }); + + return links; +} + +// Function to check if the links in .md files match the slugs in nav.ts +function checkLinks(): void { + const brokenLinks: { file: string; link: string; line: number }[] = []; + + // Extract slugs from the nav object + const validSlugs = extractSlugsFromNav(nav); + + console.log(`Extracted ${validSlugs.length} slugs from nav.ts`); + + // Get all .md files to check + const mdFiles = getMarkdownFiles(path.resolve(__dirname, '../docs')); + + mdFiles.forEach((file) => { + const links = extractLinksFromMarkdown(file); + + links.forEach(({ link, line }) => { + // Exclude external links (starting with http://, https://, mailto:, etc.) + if (/^([a-z][a-z0-9+.-]*):/.test(link)) { + // Skip the external links + // console.log(`Skipping external link: ${link}`); + return; + } + + const siteLinks = [ + '/install', + '/images', + ]; + for (const siteLink of siteLinks) { + if (link.startsWith(siteLink)) { + // Skip the site links + // console.log(`Skipping site link: ${link}`); + return; + } + } + + // For now remove the fragment part of the link and check if it is a valid slug + // TODO: Check if the fragment part references a valid heading in the file specified + // by the link + const fragmentIndex = link.indexOf('#'); + if (fragmentIndex !== -1) { + link = link.substring(0, fragmentIndex); + // If the link is empty after removing the fragment, it is + // a reference to the current file, so we skip it + if (link === '') { + return; + } + } + + if (!validSlugs.includes(link)) { + brokenLinks.push({ file, link, line }); + } + }); + }); + + if (brokenLinks.length > 0) { + console.error(`\nFound ${brokenLinks.length} broken links:`); + brokenLinks.forEach(({ file, link, line }) => { + console.error(`File: ${file}, Line: ${line}, Link: ${link}`); + }); + process.exit(1); // Exit with error if any invalid links are found + } else { + console.log('All links are valid!'); + } +} + +// Function to get all markdown files recursively +function getMarkdownFiles(dir: string): string[] { + let files: string[] = []; + const items = fs.readdirSync(dir); + + items.forEach((item) => { + const fullPath = path.join(dir, item); + const stat = fs.lstatSync(fullPath); + + if (stat.isDirectory()) { + files = files.concat(getMarkdownFiles(fullPath)); // Recurse into directories + } else if (fullPath.endsWith('.md')) { + files.push(fullPath); + } + }); + + return files; +} + +checkLinks(); diff --git a/tsconfig.json b/tsconfig.json index 2a5ee7d2..efe136bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,8 @@ "target": "ESNext", "module": "commonjs", "outDir": "./docs", + "esModuleInterop": true, + "strict": true, "skipLibCheck": true } } diff --git a/yarn.lock b/yarn.lock index fce89544..d923eebd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,196 @@ # yarn lockfile v1 +"@esbuild/aix-ppc64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz#51299374de171dbd80bb7d838e1cfce9af36f353" + integrity sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ== + +"@esbuild/android-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz#58565291a1fe548638adb9c584237449e5e14018" + integrity sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw== + +"@esbuild/android-arm@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.1.tgz#5eb8c652d4c82a2421e3395b808e6d9c42c862ee" + integrity sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ== + +"@esbuild/android-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.1.tgz#ae19d665d2f06f0f48a6ac9a224b3f672e65d517" + integrity sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg== + +"@esbuild/darwin-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz#05b17f91a87e557b468a9c75e9d85ab10c121b16" + integrity sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q== + +"@esbuild/darwin-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz#c58353b982f4e04f0d022284b8ba2733f5ff0931" + integrity sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw== + +"@esbuild/freebsd-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz#f9220dc65f80f03635e1ef96cfad5da1f446f3bc" + integrity sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA== + +"@esbuild/freebsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz#69bd8511fa013b59f0226d1609ac43f7ce489730" + integrity sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g== + +"@esbuild/linux-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz#8050af6d51ddb388c75653ef9871f5ccd8f12383" + integrity sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g== + +"@esbuild/linux-arm@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz#ecaabd1c23b701070484990db9a82f382f99e771" + integrity sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ== + +"@esbuild/linux-ia32@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz#3ed2273214178109741c09bd0687098a0243b333" + integrity sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ== + +"@esbuild/linux-loong64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz#a0fdf440b5485c81b0fbb316b08933d217f5d3ac" + integrity sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw== + +"@esbuild/linux-mips64el@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz#e11a2806346db8375b18f5e104c5a9d4e81807f6" + integrity sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q== + +"@esbuild/linux-ppc64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz#06a2744c5eaf562b1a90937855b4d6cf7c75ec96" + integrity sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw== + +"@esbuild/linux-riscv64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz#65b46a2892fc0d1af4ba342af3fe0fa4a8fe08e7" + integrity sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA== + +"@esbuild/linux-s390x@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz#e71ea18c70c3f604e241d16e4e5ab193a9785d6f" + integrity sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw== + +"@esbuild/linux-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz#d47f97391e80690d4dfe811a2e7d6927ad9eed24" + integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ== + +"@esbuild/netbsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz#44e743c9778d57a8ace4b72f3c6b839a3b74a653" + integrity sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA== + +"@esbuild/openbsd-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz#05c5a1faf67b9881834758c69f3e51b7dee015d7" + integrity sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q== + +"@esbuild/openbsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz#2e58ae511bacf67d19f9f2dcd9e8c5a93f00c273" + integrity sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA== + +"@esbuild/sunos-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz#adb022b959d18d3389ac70769cef5a03d3abd403" + integrity sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA== + +"@esbuild/win32-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz#84906f50c212b72ec360f48461d43202f4c8b9a2" + integrity sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A== + +"@esbuild/win32-ia32@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz#5e3eacc515820ff729e90d0cb463183128e82fac" + integrity sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ== + +"@esbuild/win32-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz#81fd50d11e2c32b2d6241470e3185b70c7b30699" + integrity sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg== + +"@types/node@^22.10.2": + version "22.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" + integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== + dependencies: + undici-types "~6.20.0" + +esbuild@~0.23.0: + version "0.23.1" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.1.tgz#40fdc3f9265ec0beae6f59824ade1bd3d3d2dab8" + integrity sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.23.1" + "@esbuild/android-arm" "0.23.1" + "@esbuild/android-arm64" "0.23.1" + "@esbuild/android-x64" "0.23.1" + "@esbuild/darwin-arm64" "0.23.1" + "@esbuild/darwin-x64" "0.23.1" + "@esbuild/freebsd-arm64" "0.23.1" + "@esbuild/freebsd-x64" "0.23.1" + "@esbuild/linux-arm" "0.23.1" + "@esbuild/linux-arm64" "0.23.1" + "@esbuild/linux-ia32" "0.23.1" + "@esbuild/linux-loong64" "0.23.1" + "@esbuild/linux-mips64el" "0.23.1" + "@esbuild/linux-ppc64" "0.23.1" + "@esbuild/linux-riscv64" "0.23.1" + "@esbuild/linux-s390x" "0.23.1" + "@esbuild/linux-x64" "0.23.1" + "@esbuild/netbsd-x64" "0.23.1" + "@esbuild/openbsd-arm64" "0.23.1" + "@esbuild/openbsd-x64" "0.23.1" + "@esbuild/sunos-x64" "0.23.1" + "@esbuild/win32-arm64" "0.23.1" + "@esbuild/win32-ia32" "0.23.1" + "@esbuild/win32-x64" "0.23.1" + +fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +get-tsconfig@^4.7.5: + version "4.8.1" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.8.1.tgz#8995eb391ae6e1638d251118c7b56de7eb425471" + integrity sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg== + dependencies: + resolve-pkg-maps "^1.0.0" + +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +tsx@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.19.2.tgz#2d7814783440e0ae42354d0417d9c2989a2ae92c" + integrity sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g== + dependencies: + esbuild "~0.23.0" + get-tsconfig "^4.7.5" + optionalDependencies: + fsevents "~2.3.3" + typescript@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.2.tgz#00d1c7c1c46928c5845c1ee8d0cc2791031d4c43" integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ== + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== From bb92966904005ae8140c17135ecc0ab2890f0ff5 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 31 Dec 2024 18:08:04 -0500 Subject: [PATCH 02/15] Removed erroneous thing --- nav.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/nav.ts b/nav.ts index c4ae2232..8ca41be7 100644 --- a/nav.ts +++ b/nav.ts @@ -32,7 +32,6 @@ const nav: Nav = { section('Intro'), page('Overview', 'index', 'index.md'), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? page('Getting Started', 'getting-started', 'getting-started.md'), - page('asdfasdfStarted', 'getting-started', 'getting-started.md'), section('Deploying'), page('Testnet', 'deploying/testnet', 'deploying/testnet.md'), From c52b49f73170966a752dc29cecee15cc6c9d6a07 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 31 Dec 2024 18:13:19 -0500 Subject: [PATCH 03/15] Switched the action trigger --- .github/workflows/check-links.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index ffdc10df..1053fe7d 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -2,9 +2,8 @@ name: Check Link Validity in Documentation on: pull_request: - paths: - - '**/*.md' - - 'nav.ts' + branches: + - master jobs: check-links: From 76fc466f0f44c35700f568f52b6e00f1aa00c8bd Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 31 Dec 2024 18:18:47 -0500 Subject: [PATCH 04/15] Added workflow to ensure that the nav.ts has been built to nav.js --- .github/workflows/validate-nav-build.yml | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/validate-nav-build.yml diff --git a/.github/workflows/validate-nav-build.yml b/.github/workflows/validate-nav-build.yml new file mode 100644 index 00000000..044f1a3a --- /dev/null +++ b/.github/workflows/validate-nav-build.yml @@ -0,0 +1,35 @@ +name: Validate nav.ts Matches nav.js + branches: + - master + +on: + pull_request: + +jobs: + validate-build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16' + + - name: Install dependencies + run: | + npm ci + + - name: Build nav.ts + run: | + npm run build + + - name: Compare generated nav.js with committed nav.js + run: | + diff -q docs/nav.js docs/nav.js.built || (echo "Generated nav.js differs from committed version. Run 'npm run build' and commit the updated file." && exit 1) + + - name: Success message + if: success() + run: echo "nav.ts builds identically to the committed nav.js" From 0134b645a4ebb56a2afdad8be33830dfa8bd3c62 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 31 Dec 2024 18:19:57 -0500 Subject: [PATCH 05/15] typo --- .github/workflows/validate-nav-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-nav-build.yml b/.github/workflows/validate-nav-build.yml index 044f1a3a..ee3e585f 100644 --- a/.github/workflows/validate-nav-build.yml +++ b/.github/workflows/validate-nav-build.yml @@ -1,9 +1,9 @@ name: Validate nav.ts Matches nav.js - branches: - - master on: pull_request: + branches: + - master jobs: validate-build: From ae954e3fcace6bac31ad307446460425209dd8fd Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 31 Dec 2024 18:21:02 -0500 Subject: [PATCH 06/15] Build nav.ts --- docs/nav.js | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/nav.js b/docs/nav.js index 8d368615..5c3a920e 100644 --- a/docs/nav.js +++ b/docs/nav.js @@ -22,7 +22,6 @@ var nav = { section('Intro'), page('Overview', 'index', 'index.md'), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? page('Getting Started', 'getting-started', 'getting-started.md'), - page('asdfasdfStarted', 'getting-started', 'getting-started.md'), section('Deploying'), page('Testnet', 'deploying/testnet', 'deploying/testnet.md'), section('Migration Guides'), From c524d721b769a292cc29eb2cc177ec26774057b9 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 31 Dec 2024 18:22:11 -0500 Subject: [PATCH 07/15] typo thing --- .github/workflows/validate-nav-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-nav-build.yml b/.github/workflows/validate-nav-build.yml index ee3e585f..88b602de 100644 --- a/.github/workflows/validate-nav-build.yml +++ b/.github/workflows/validate-nav-build.yml @@ -20,7 +20,7 @@ jobs: - name: Install dependencies run: | - npm ci + npm install - name: Build nav.ts run: | From d5e979a3afc1119d8e72dd419199b209393f776f Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 31 Dec 2024 18:25:21 -0500 Subject: [PATCH 08/15] Fixed script issue --- .github/workflows/validate-nav-build.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate-nav-build.yml b/.github/workflows/validate-nav-build.yml index 88b602de..3485847f 100644 --- a/.github/workflows/validate-nav-build.yml +++ b/.github/workflows/validate-nav-build.yml @@ -20,16 +20,21 @@ jobs: - name: Install dependencies run: | - npm install + npm ci + + - name: Backup existing nav.js + run: | + mv docs/nav.js docs/nav.js.original - name: Build nav.ts run: | npm run build - - name: Compare generated nav.js with committed nav.js + - name: Compare generated nav.js with original nav.js run: | - diff -q docs/nav.js docs/nav.js.built || (echo "Generated nav.js differs from committed version. Run 'npm run build' and commit the updated file." && exit 1) + diff -q docs/nav.js docs/nav.js.original || (echo "Generated nav.js differs from committed version. Run 'npm run build' and commit the updated file." && exit 1) - - name: Success message - if: success() - run: echo "nav.ts builds identically to the committed nav.js" + - name: Restore original nav.js + if: success() || failure() + run: | + mv docs/nav.js.original docs/nav.js From e09f2593cc18c6620861dd5449ef162bbb747208 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 31 Dec 2024 18:26:20 -0500 Subject: [PATCH 09/15] Fix --- .github/workflows/validate-nav-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-nav-build.yml b/.github/workflows/validate-nav-build.yml index 3485847f..b76378d6 100644 --- a/.github/workflows/validate-nav-build.yml +++ b/.github/workflows/validate-nav-build.yml @@ -20,7 +20,7 @@ jobs: - name: Install dependencies run: | - npm ci + npm install - name: Backup existing nav.js run: | From fb74ed6a1569175f85f11dacbcd4d0a727df7810 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 31 Dec 2024 18:31:50 -0500 Subject: [PATCH 10/15] Fixed a few links --- docs/bsatn.md | 2 +- docs/modules/c-sharp/quickstart.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/bsatn.md b/docs/bsatn.md index 0da55ce7..31c9161f 100644 --- a/docs/bsatn.md +++ b/docs/bsatn.md @@ -110,6 +110,6 @@ Where All SATS types are BSATN-encoded by converting them to an `AlgebraicValue`, then BSATN-encoding that meta-value. -See [the SATN JSON Format](/docs/satn-reference-json-format) +See [the SATN JSON Format](/docs/satn) for more details of the conversion to meta values. Note that these meta values are converted to BSATN and _not JSON_. diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index 5d8c873d..571351c1 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -312,6 +312,6 @@ spacetime sql "SELECT * FROM Message" ## What's next? -You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quick start guide for your client language of choice: [Rust](/docs/languages/rust/rust-sdk-quickstart-guide), [C#](/docs/languages/csharp/csharp-sdk-quickstart-guide), or [TypeScript](/docs/languages/typescript/typescript-sdk-quickstart-guide). +You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quick start guide for your client language of choice: [Rust](/docs/sdks/rust/quickstart), [C#](/docs/sdks/c-sharp/quickstart), or [TypeScript](/docs/sdks/typescript/quickstart). If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](/docs/unity/part-1) or check out our example game, [BitcraftMini](/docs/unity/part-3). From b8c904b668b427055cc9a2c60feac6a3a131e7c8 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Wed, 1 Jan 2025 17:18:38 -0500 Subject: [PATCH 11/15] Added relative link resolution and fixed the broken links --- docs/sdks/c-sharp/index.md | 7 +++---- docs/unity/part-1.md | 4 ++-- docs/ws/index.md | 2 +- scripts/checkLinks.ts | 37 +++++++++++++++++++++++-------------- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/docs/sdks/c-sharp/index.md b/docs/sdks/c-sharp/index.md index d85f5702..37b45b7f 100644 --- a/docs/sdks/c-sharp/index.md +++ b/docs/sdks/c-sharp/index.md @@ -16,10 +16,10 @@ The SpacetimeDB client C# for Rust contains all the tools you need to build nati - [Method `SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect) - [Event `SpacetimeDBClient.onIdentityReceived`](#event-spacetimedbclientonidentityreceived) - [Event `SpacetimeDBClient.onConnect`](#event-spacetimedbclientonconnect) - - [Query subscriptions & one-time actions](#subscribe-to-queries) + - [Subscribe to queries](#subscribe-to-queries) - [Method `SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe) - [Event `SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied) - - [Method `SpacetimeDBClient.OneOffQuery`](#method-spacetimedbclientoneoffquery) + - [Method \[`SpacetimeDBClient.OneOffQuery`\]](#method-spacetimedbclientoneoffquery) - [View rows of subscribed tables](#view-rows-of-subscribed-tables) - [Class `{TABLE}`](#class-table) - [Static Method `{TABLE}.Iter`](#static-method-tableiter) @@ -45,7 +45,6 @@ The SpacetimeDB client C# for Rust contains all the tools you need to build nati - [Static Property `AuthToken.Token`](#static-property-authtokentoken) - [Static Method `AuthToken.SaveToken`](#static-method-authtokensavetoken) - [Class `Identity`](#class-identity) - - [Class `Identity`](#class-identity-1) - [Customizing logging](#customizing-logging) - [Interface `ISpacetimeDBLogger`](#interface-ispacetimedblogger) - [Class `ConsoleLogger`](#class-consolelogger) @@ -104,7 +103,7 @@ The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in ![Unity-AddNetworkManager](/images/unity-tutorial/Unity-AddNetworkManager.JPG) -This component will handle updating and closing the [`SpacetimeDBClient.instance`](#property-spacetimedbclientinstance) for you, but will not call [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect), you still need to handle that yourself. See the [Unity Quickstart](./UnityQuickStart) and [Unity Tutorial](./UnityTutorialPart1) for more information. +This component will handle updating and closing the [`SpacetimeDBClient.instance`](#property-spacetimedbclientinstance) for you, but will not call [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect), you still need to handle that yourself. See the [Unity Tutorial](/docs/unity-tutorial) for more information. ### Method `SpacetimeDBClient.Connect` diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 8e0a49e3..10967b33 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -119,5 +119,5 @@ We chose ECS for this example project because it promotes scalability, modularit From here, the tutorial continues with your favorite server module language of choice: -- [Rust](part-2a-rust.md) -- [C#](part-2b-csharp.md) +- [Rust](part-2a-rust) +- [C#](part-2b-c-sharp) diff --git a/docs/ws/index.md b/docs/ws/index.md index 587fbad0..1a3780cc 100644 --- a/docs/ws/index.md +++ b/docs/ws/index.md @@ -1,6 +1,6 @@ # The SpacetimeDB WebSocket API -As an extension of the [HTTP API](/doc/http-api-reference), SpacetimeDB offers a WebSocket API. Clients can subscribe to a database via a WebSocket connection to receive streaming updates as the database changes, and send requests to invoke reducers. Messages received from the server over a WebSocket will follow the same total ordering of transactions as are committed to the database. +As an extension of the [HTTP API](/docs/http), SpacetimeDB offers a WebSocket API. Clients can subscribe to a database via a WebSocket connection to receive streaming updates as the database changes, and send requests to invoke reducers. Messages received from the server over a WebSocket will follow the same total ordering of transactions as are committed to the database. The SpacetimeDB SDKs comminicate with their corresponding database using the WebSocket API. diff --git a/scripts/checkLinks.ts b/scripts/checkLinks.ts index 2813e58c..9e3709b8 100644 --- a/scripts/checkLinks.ts +++ b/scripts/checkLinks.ts @@ -37,6 +37,20 @@ function extractLinksFromMarkdown(filePath: string): { link: string; line: numbe return links; } +// Resolve relative links based on the current file's location +function resolveLink(link: string, filePath: string): string { + // If the link is absolute (starts with `/`), return it as is + if (link.startsWith('/')) { + return link; + } + // Resolve the relative link to an absolute path + const fileDir = path.dirname(filePath); + const resolvedPath = path.join(fileDir, link); + // Convert to a normalized format (e.g., `/docs/...`) + const relativePath = path.relative(path.resolve(__dirname, '../docs'), resolvedPath); + return `/docs/${relativePath}`; +} + // Function to check if the links in .md files match the slugs in nav.ts function checkLinks(): void { const brokenLinks: { file: string; link: string; line: number }[] = []; @@ -56,37 +70,32 @@ function checkLinks(): void { // Exclude external links (starting with http://, https://, mailto:, etc.) if (/^([a-z][a-z0-9+.-]*):/.test(link)) { // Skip the external links - // console.log(`Skipping external link: ${link}`); return; } - const siteLinks = [ - '/install', - '/images', - ]; + const siteLinks = ['/install', '/images']; for (const siteLink of siteLinks) { if (link.startsWith(siteLink)) { // Skip the site links - // console.log(`Skipping site link: ${link}`); return; } } // For now remove the fragment part of the link and check if it is a valid slug - // TODO: Check if the fragment part references a valid heading in the file specified - // by the link const fragmentIndex = link.indexOf('#'); if (fragmentIndex !== -1) { link = link.substring(0, fragmentIndex); - // If the link is empty after removing the fragment, it is - // a reference to the current file, so we skip it if (link === '') { + // Skip references to the current file return; } } - - if (!validSlugs.includes(link)) { - brokenLinks.push({ file, link, line }); + + // Resolve the link to its absolute counterpart + const resolvedLink = resolveLink(link, file); + + if (!validSlugs.includes(resolvedLink)) { + brokenLinks.push({ file, link: resolvedLink, line }); } }); }); @@ -94,7 +103,7 @@ function checkLinks(): void { if (brokenLinks.length > 0) { console.error(`\nFound ${brokenLinks.length} broken links:`); brokenLinks.forEach(({ file, link, line }) => { - console.error(`File: ${file}, Line: ${line}, Link: ${link}`); + console.error(`File: ${file}:${line}, Link: ${link}`); }); process.exit(1); // Exit with error if any invalid links are found } else { From 0101cf41088309bd29250f2e3e49255a48f89306 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Wed, 1 Jan 2025 18:03:37 -0500 Subject: [PATCH 12/15] now checking fragments --- scripts/checkLinks.ts | 118 ++++++++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 32 deletions(-) diff --git a/scripts/checkLinks.ts b/scripts/checkLinks.ts index 9e3709b8..8561ec57 100644 --- a/scripts/checkLinks.ts +++ b/scripts/checkLinks.ts @@ -2,14 +2,15 @@ import fs from 'fs'; import path from 'path'; import nav from '../nav'; // Import the nav object directly -// Function to extract slugs from the nav object and prefix them with /docs -function extractSlugsFromNav(nav: { items: any[] }): string[] { - const slugs: string[] = []; +// Function to map slugs to file paths from nav.ts +function extractSlugToPathMap(nav: { items: any[] }): Map { + const slugToPath = new Map(); function traverseNav(items: any[]): void { items.forEach((item) => { - if (item.type === 'page' && item.slug) { - slugs.push(`/docs/${item.slug}`); // Prefix slugs with /docs + if (item.type === 'page' && item.slug && item.path) { + const resolvedPath = path.resolve(__dirname, '../docs', item.path); + slugToPath.set(`/docs/${item.slug}`, resolvedPath); } else if (item.type === 'section' && item.items) { traverseNav(item.items); // Recursively traverse sections } @@ -17,7 +18,16 @@ function extractSlugsFromNav(nav: { items: any[] }): string[] { } traverseNav(nav.items); - return slugs; + return slugToPath; +} + +// Function to assert that all files in slugToPath exist +function validatePathsExist(slugToPath: Map): void { + slugToPath.forEach((filePath, slug) => { + if (!fs.existsSync(filePath)) { + throw new Error(`File not found: ${filePath} (Referenced by slug: ${slug})`); + } + }); } // Function to extract links from markdown files with line numbers @@ -37,28 +47,64 @@ function extractLinksFromMarkdown(filePath: string): { link: string; line: numbe return links; } -// Resolve relative links based on the current file's location +// Function to resolve relative links based on the current file's location function resolveLink(link: string, filePath: string): string { - // If the link is absolute (starts with `/`), return it as is + if (link.startsWith('#')) { + // If the link is a fragment, resolve it to the current file + const currentSlug = `/docs/${path.relative( + path.resolve(__dirname, '../docs'), + filePath + ).replace(/\\/g, '/')}`.replace(/\.md$/, ''); // Normalize to slug format + return `${currentSlug}${link}`; + } + if (link.startsWith('/')) { - return link; + return link; // Absolute links are already resolved } - // Resolve the relative link to an absolute path + const fileDir = path.dirname(filePath); const resolvedPath = path.join(fileDir, link); - // Convert to a normalized format (e.g., `/docs/...`) const relativePath = path.relative(path.resolve(__dirname, '../docs'), resolvedPath); - return `/docs/${relativePath}`; + return `/docs/${relativePath}`; // Ensure resolved links are prefixed with /docs } -// Function to check if the links in .md files match the slugs in nav.ts +// Function to extract headings from a markdown file +function extractHeadingsFromMarkdown(filePath: string): string[] { + if (!fs.existsSync(filePath) || !fs.lstatSync(filePath).isFile()) { + return []; // Return an empty list if the file does not exist or is not a file + } + + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const headingRegex = /^(#{1,6})\s+(.*)$/gm; // Match markdown headings like # Heading + const headings: string[] = []; + let match: RegExpExecArray | null; + + while ((match = headingRegex.exec(fileContent)) !== null) { + const heading = match[2].trim(); // Extract the heading text + const slug = heading + .toLowerCase() + .replace(/[^\w\- ]+/g, '') // Remove special characters + .replace(/\s+/g, '-'); // Replace spaces with hyphens + headings.push(slug); + } + + return headings; +} + +// Function to check if the links in .md files match the slugs in nav.ts and validate fragments function checkLinks(): void { const brokenLinks: { file: string; link: string; line: number }[] = []; - // Extract slugs from the nav object - const validSlugs = extractSlugsFromNav(nav); + // Extract the slug-to-path mapping from nav.ts + const slugToPath = extractSlugToPathMap(nav); + + // Validate that all paths in slugToPath exist + validatePathsExist(slugToPath); + + console.log(`Validated ${slugToPath.size} paths from nav.ts`); - console.log(`Extracted ${validSlugs.length} slugs from nav.ts`); + // Extract valid slugs + const validSlugs = Array.from(slugToPath.keys()); // Get all .md files to check const mdFiles = getMarkdownFiles(path.resolve(__dirname, '../docs')); @@ -69,33 +115,41 @@ function checkLinks(): void { links.forEach(({ link, line }) => { // Exclude external links (starting with http://, https://, mailto:, etc.) if (/^([a-z][a-z0-9+.-]*):/.test(link)) { - // Skip the external links - return; + return; // Skip external links } const siteLinks = ['/install', '/images']; for (const siteLink of siteLinks) { if (link.startsWith(siteLink)) { - // Skip the site links - return; - } - } - - // For now remove the fragment part of the link and check if it is a valid slug - const fragmentIndex = link.indexOf('#'); - if (fragmentIndex !== -1) { - link = link.substring(0, fragmentIndex); - if (link === '') { - // Skip references to the current file - return; + return; // Skip site links } } - // Resolve the link to its absolute counterpart + // Resolve the link const resolvedLink = resolveLink(link, file); - if (!validSlugs.includes(resolvedLink)) { + // Split the resolved link into base and fragment + const [baseLink, fragmentRaw] = resolvedLink.split('#'); + const fragment: string | null = fragmentRaw || null; + + // Check if the base link matches a valid slug + if (!validSlugs.includes(baseLink)) { brokenLinks.push({ file, link: resolvedLink, line }); + return; + } + + // Validate the fragment, if present + if (fragment) { + const targetFile = slugToPath.get(baseLink); + if (targetFile) { + const targetHeadings = extractHeadingsFromMarkdown(targetFile); + + if (!targetHeadings.includes(fragment)) { + brokenLinks.push({ file, link: resolvedLink, line }); + } else { + console.log(`Found valid link: ${file}:${line} ${resolvedLink}`); + } + } } }); }); From ba7a1a9a4d5021e1d9947b48c923b0a42aadb0ce Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Wed, 1 Jan 2025 18:29:55 -0500 Subject: [PATCH 13/15] Now checking fragments properly and publishing some stats --- scripts/checkLinks.ts | 72 +++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/scripts/checkLinks.ts b/scripts/checkLinks.ts index 8561ec57..a7dad88f 100644 --- a/scripts/checkLinks.ts +++ b/scripts/checkLinks.ts @@ -47,25 +47,22 @@ function extractLinksFromMarkdown(filePath: string): { link: string; line: numbe return links; } -// Function to resolve relative links based on the current file's location -function resolveLink(link: string, filePath: string): string { +// Function to resolve relative links using slugs +function resolveLink(link: string, currentSlug: string): string { if (link.startsWith('#')) { - // If the link is a fragment, resolve it to the current file - const currentSlug = `/docs/${path.relative( - path.resolve(__dirname, '../docs'), - filePath - ).replace(/\\/g, '/')}`.replace(/\.md$/, ''); // Normalize to slug format + // If the link is a fragment, resolve it to the current slug return `${currentSlug}${link}`; } if (link.startsWith('/')) { - return link; // Absolute links are already resolved + // Absolute links are returned as-is + return link; } - const fileDir = path.dirname(filePath); - const resolvedPath = path.join(fileDir, link); - const relativePath = path.relative(path.resolve(__dirname, '../docs'), resolvedPath); - return `/docs/${relativePath}`; // Ensure resolved links are prefixed with /docs + // Resolve relative links based on slug + const currentSlugDir = path.dirname(currentSlug); + const resolvedSlug = path.normalize(path.join(currentSlugDir, link)).replace(/\\/g, '/'); + return resolvedSlug.startsWith('/docs') ? resolvedSlug : `/docs${resolvedSlug}`; } // Function to extract headings from a markdown file @@ -94,6 +91,14 @@ function extractHeadingsFromMarkdown(filePath: string): string[] { // Function to check if the links in .md files match the slugs in nav.ts and validate fragments function checkLinks(): void { const brokenLinks: { file: string; link: string; line: number }[] = []; + let totalFiles = 0; + let totalLinks = 0; + let validLinks = 0; + let invalidLinks = 0; + let totalFragments = 0; + let validFragments = 0; + let invalidFragments = 0; + let currentFileFragments = 0; // Extract the slug-to-path mapping from nav.ts const slugToPath = extractSlugToPathMap(nav); @@ -106,11 +111,22 @@ function checkLinks(): void { // Extract valid slugs const validSlugs = Array.from(slugToPath.keys()); + // Reverse map from file path to slug for current file resolution + const pathToSlug = new Map(); + slugToPath.forEach((filePath, slug) => { + pathToSlug.set(filePath, slug); + }); + // Get all .md files to check const mdFiles = getMarkdownFiles(path.resolve(__dirname, '../docs')); + totalFiles = mdFiles.length; + mdFiles.forEach((file) => { const links = extractLinksFromMarkdown(file); + totalLinks += links.length; + + const currentSlug = pathToSlug.get(file) || ''; links.forEach(({ link, line }) => { // Exclude external links (starting with http://, https://, mailto:, etc.) @@ -125,17 +141,25 @@ function checkLinks(): void { } } - // Resolve the link - const resolvedLink = resolveLink(link, file); + // Resolve the link + const resolvedLink = resolveLink(link, currentSlug); + // Split the resolved link into base and fragment const [baseLink, fragmentRaw] = resolvedLink.split('#'); const fragment: string | null = fragmentRaw || null; + if (fragment) { + totalFragments += 1; + } + // Check if the base link matches a valid slug if (!validSlugs.includes(baseLink)) { brokenLinks.push({ file, link: resolvedLink, line }); + invalidLinks += 1; return; + } else { + validLinks += 1; } // Validate the fragment, if present @@ -146,8 +170,13 @@ function checkLinks(): void { if (!targetHeadings.includes(fragment)) { brokenLinks.push({ file, link: resolvedLink, line }); + invalidFragments += 1; + invalidLinks += 1; } else { - console.log(`Found valid link: ${file}:${line} ${resolvedLink}`); + validFragments += 1; + if (baseLink === currentSlug) { + currentFileFragments += 1; + } } } } @@ -159,10 +188,21 @@ function checkLinks(): void { brokenLinks.forEach(({ file, link, line }) => { console.error(`File: ${file}:${line}, Link: ${link}`); }); - process.exit(1); // Exit with error if any invalid links are found } else { console.log('All links are valid!'); } + + // Print statistics + console.log('\n=== Link Validation Statistics ==='); + console.log(`Total markdown files processed: ${totalFiles}`); + console.log(`Total links processed: ${totalLinks}`); + console.log(` Valid links: ${validLinks}`); + console.log(` Invalid links: ${invalidLinks}`); + console.log(`Total links with fragments processed: ${totalFragments}`); + console.log(` Valid links with fragments: ${validFragments}`); + console.log(` Invalid links with fragments: ${invalidFragments}`); + console.log(`Fragments referring to the current file: ${currentFileFragments}`); + console.log('================================='); } // Function to get all markdown files recursively From 1528390e442c31eeff19fbf71d9bd2d851b303b6 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Wed, 1 Jan 2025 18:35:33 -0500 Subject: [PATCH 14/15] Forgot exit code --- scripts/checkLinks.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/checkLinks.ts b/scripts/checkLinks.ts index a7dad88f..78a8daf8 100644 --- a/scripts/checkLinks.ts +++ b/scripts/checkLinks.ts @@ -203,6 +203,10 @@ function checkLinks(): void { console.log(` Invalid links with fragments: ${invalidFragments}`); console.log(`Fragments referring to the current file: ${currentFileFragments}`); console.log('================================='); + + if (brokenLinks.length > 0) { + process.exit(1); // Exit with an error code if there are broken links + } } // Function to get all markdown files recursively From 7653b497ac81855e659ab5965cd18b304ecad0da Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Thu, 2 Jan 2025 10:26:56 -0500 Subject: [PATCH 15/15] Fix broken links Well, in at least some cases, just remove broken links. - The BSATN ref contained links to type defns, but didn't have type defns. Replace the links with plain text. - HTTP database links for recovery-code related routes were getting mangled in some way I couldn't figure out, so the links weren't working despite their targets clearly existing. Conveniently, those routes have been removed, so remove the links and the corresponding sections. - The JSON doc (erroneously called "SATN") contained typos, spelling "producttype" as "productype". - C# SDK ref had links to a section on the `Address` type, but no such section. Replace the links with plain text. - Rust SDK ref had a link getting mangled in a way I couldn't figure out. Simplify the section title so that the anchor name is predictable. - TypeSciprt SDK ref used camelCase names in anchor links, but we downcase all section titles to create anchor names. Also slap a section in README.md which says how to run the checker locally. --- README.md | 4 ++++ docs/bsatn.md | 30 +++++++++++---------------- docs/http/database.md | 39 ----------------------------------- docs/satn.md | 4 ++-- docs/sdks/c-sharp/index.md | 2 +- docs/sdks/rust/index.md | 4 ++-- docs/sdks/typescript/index.md | 9 ++++---- 7 files changed, 25 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index c31b2c3f..2165ae62 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ git push -u origin a-branch-name-that-describes-my-change > NOTE! If you make a change to `nav.ts` you will have to run `npm run build` to generate a new `docs/nav.js` file. +### Checking Links + +We have a CI job which validates internal links. You can run it locally with `npm run check-links`. This will print any internal links (i.e. links to other docs pages) whose targets do not exist, including fragment links (i.e. `#`-ey links to anchors). + ## License This documentation repository is licensed under Apache 2.0. See LICENSE.txt for more details. diff --git a/docs/bsatn.md b/docs/bsatn.md index 31c9161f..e8e6d945 100644 --- a/docs/bsatn.md +++ b/docs/bsatn.md @@ -24,12 +24,12 @@ To do this, we use inductive definitions, and define the following notation: ### At a glance -| Type | Description | -| ---------------- | ---------------------------------------------------------------- | -| `AlgebraicValue` | A value whose type may be any [`AlgebraicType`](#algebraictype). | -| `SumValue` | A value whose type is a [`SumType`](#sumtype). | -| `ProductValue` | A value whose type is a [`ProductType`](#producttype). | -| `BuiltinValue` | A value whose type is a [`BuiltinType`](#builtintype). | +| Type | Description | +|-------------------------------------|-----------------------------------------------------------------------| +| [`AlgebraicValue`](#algebraicvalue) | A value of any type. | +| [`SumValue`](#sumvalue) | A value of a sum type, i.e. an enum or tagged union. | +| [`ProductValue`](#productvalue) | A value of a product type, i.e. a struct or tuple. | +| [`BuiltinValue`](#builtinvalue) | A value of a builtin type, including numbers, booleans and sequences. | ### `AlgebraicValue` @@ -41,17 +41,17 @@ bsatn(AlgebraicValue) = bsatn(SumValue) | bsatn(ProductValue) | bsatn(BuiltinVal ### `SumValue` -An instance of a [`SumType`](#sumtype). +An instance of a sum type, i.e. an enum or tagged union. `SumValue`s are binary-encoded as `bsatn(tag) ++ bsatn(variant_data)` -where `tag: u8` is an index into the [`SumType.variants`](#sumtype) -array of the value's [`SumType`](#sumtype), +where `tag: u8` is an index into the `SumType.variants` +array of the value's `SumType`, and where `variant_data` is the data of the variant. For variants holding no data, i.e., of some zero sized type, `bsatn(variant_data) = []`. ### `ProductValue` -An instance of a [`ProductType`](#producttype). +An instance of a product type, i.e. a struct or tuple. `ProductValue`s are binary encoded as: ```fsharp @@ -62,7 +62,8 @@ Field names are not encoded. ### `BuiltinValue` -An instance of a [`BuiltinType`](#builtintype). +An instance of a buil-in type. +Built-in types include booleans, integers, floats, strings and arrays. The BSATN encoding of `BuiltinValue`s defers to the encoding of each variant: ```fsharp @@ -73,7 +74,6 @@ bsatn(BuiltinValue) | bsatn(F32) | bsatn(F64) | bsatn(String) | bsatn(Array) - | bsatn(Map) bsatn(Bool(b)) = bsatn(b as u8) bsatn(U8(x)) = [x] @@ -91,10 +91,6 @@ bsatn(F64(x: f64)) = bsatn(f64_to_raw_bits(x)) // lossless conversion bsatn(String(s)) = bsatn(len(s) as u32) ++ bsatn(bytes(s)) bsatn(Array(a)) = bsatn(len(a) as u32) ++ bsatn(normalize(a)_0) ++ .. ++ bsatn(normalize(a)_n) -bsatn(Map(map)) = bsatn(len(m) as u32) - ++ bsatn(key(map_0)) ++ bsatn(value(map_0)) - .. - ++ bsatn(key(map_n)) ++ bsatn(value(map_n)) ``` Where @@ -102,8 +98,6 @@ Where - `f32_to_raw_bits(x)` is the raw transmute of `x: f32` to `u32` - `f64_to_raw_bits(x)` is the raw transmute of `x: f64` to `u64` - `normalize(a)` for `a: ArrayValue` converts `a` to a list of `AlgebraicValue`s -- `key(map_i)` extracts the key of the `i`th entry of `map` -- `value(map_i)` extracts the value of the `i`th entry of `map` ## Types diff --git a/docs/http/database.md b/docs/http/database.md index 9b6e0488..b23701e8 100644 --- a/docs/http/database.md +++ b/docs/http/database.md @@ -11,8 +11,6 @@ The HTTP endpoints in `/database` allow clients to interact with Spacetime datab | [`/database/set_name GET`](#databaseset_name-get) | Set a database's name, given its address. | | [`/database/ping GET`](#databaseping-get) | No-op. Used to determine whether a client can connect. | | [`/database/register_tld GET`](#databaseregister_tld-get) | Register a top-level domain. | -| [`/database/request_recovery_code GET`](#databaserequest_recovery_code-get) | Request a recovery code to the email associated with an identity. | -| [`/database/confirm_recovery_code GET`](#databaseconfirm_recovery_code-get) | Recover a login token from a recovery code. | | [`/database/publish POST`](#databasepublish-post) | Publish a database given its module code. | | [`/database/delete/:address POST`](#databasedeleteaddress-post) | Delete a database. | | [`/database/subscribe/:name_or_address GET`](#databasesubscribename_or_address-get) | Begin a [WebSocket connection](/docs/ws). | @@ -175,43 +173,6 @@ If the domain is already registered to another identity, returns JSON in the for } } ``` -## `/database/request_recovery_code GET` - -Request a recovery code or link via email, in order to recover the token associated with an identity. - -Accessible through the CLI as `spacetime identity recover `. - -#### Query Parameters - -| Name | Value | -| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `identity` | The identity whose token should be recovered. | -| `email` | The email to send the recovery code or link to. This email must be associated with the identity, either during creation via [`/identity`](/docs/http/identity#identity-post) or afterwards via [`/identity/:identity/set-email`](/docs/http/identity#identityidentityset_email-post). | -| `link` | A boolean; whether to send a clickable link rather than a recovery code. | - -## `/database/confirm_recovery_code GET` - -Confirm a recovery code received via email following a [`/database/request_recovery_code GET`](#-database-request_recovery_code-get) request, and retrieve the identity's token. - -Accessible through the CLI as `spacetime identity recover `. - -#### Query Parameters - -| Name | Value | -| ---------- | --------------------------------------------- | -| `identity` | The identity whose token should be recovered. | -| `email` | The email which received the recovery code. | -| `code` | The recovery code received via email. | - -On success, returns JSON in the form: - -```typescript -{ - "identity": string, - "token": string -} -``` - ## `/database/publish POST` Publish a database. diff --git a/docs/satn.md b/docs/satn.md index f21e9b30..6fb0ee9f 100644 --- a/docs/satn.md +++ b/docs/satn.md @@ -34,7 +34,7 @@ The tag is an index into the [`SumType.variants`](#sumtype) array of the value's ### `ProductValue` -An instance of a [`ProductType`](#producttype). `ProductValue`s are encoded as JSON arrays. Each element of the `ProductValue` array is of the type of the corresponding index in the [`ProductType.elements`](#productype) array of the value's [`ProductType`](#producttype). +An instance of a [`ProductType`](#producttype). `ProductValue`s are encoded as JSON arrays. Each element of the `ProductValue` array is of the type of the corresponding index in the [`ProductType.elements`](#producttype) array of the value's [`ProductType`](#producttype). ```json array @@ -69,7 +69,7 @@ All SATS types are JSON-encoded by converting them to an `AlgebraicValue`, then | --------------------------------------- | ------------------------------------------------------------------------------------ | | [`AlgebraicType`](#algebraictype) | Any SATS type. | | [`SumType`](#sumtype) | Sum types, i.e. tagged unions. | -| [`ProductType`](#productype) | Product types, i.e. structures. | +| [`ProductType`](#producttype) | Product types, i.e. structures. | | [`BuiltinType`](#builtintype) | Built-in and primitive types, including booleans, numbers, strings, arrays and maps. | | [`AlgebraicTypeRef`](#algebraictyperef) | An indirect reference to a type, used to implement recursive types. | diff --git a/docs/sdks/c-sharp/index.md b/docs/sdks/c-sharp/index.md index 37b45b7f..a044e4ea 100644 --- a/docs/sdks/c-sharp/index.md +++ b/docs/sdks/c-sharp/index.md @@ -171,7 +171,7 @@ class SpacetimeDBClient { } ``` -Called when we receive an auth token, [`Identity`](#class-identity) and [`Address`](#class-address) from the server. The [`Identity`](#class-identity) serves as a unique public identifier for a user of the database. It can be for several purposes, such as filtering rows in a database for the rows created by a particular user. The auth token is a private access token that allows us to assume an identity. The [`Address`](#class-address) is opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity). +Called when we receive an auth token, [`Identity`](#class-identity) and `Address` from the server. The [`Identity`](#class-identity) serves as a unique public identifier for a user of the database. It can be for several purposes, such as filtering rows in a database for the rows created by a particular user. The auth token is a private access token that allows us to assume an identity. The `Address` is opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity). To store the auth token to the filesystem, use the static method [`AuthToken.SaveToken`](#static-method-authtokensavetoken). You may also want to store the returned [`Identity`](#class-identity) in a local variable. diff --git a/docs/sdks/rust/index.md b/docs/sdks/rust/index.md index 50e8aa9b..d8befe53 100644 --- a/docs/sdks/rust/index.md +++ b/docs/sdks/rust/index.md @@ -149,7 +149,7 @@ impl DbConnection { `frame_tick` will advance the connection until no work remains, then return rather than blocking or `await`-ing. Games might arrange for this message to be called every frame. `frame_tick` returns `Ok` if the connection remains active afterwards, or `Err` if the connection disconnected before or during the call. -## Trait `spacetimedb_sdk::DbContext` +## Trait `DbContext` [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext) both implement `DbContext`, which allows @@ -185,7 +185,7 @@ impl SubscriptionBuilder { } ``` -Register a callback to run when the subscription is applied and the matching rows are inserted into the client cache. The [`EventContext`](#type-module_bindings-eventcontext) passed to the callback will have `Event::SubscribeApplied` as its `event`. +Register a callback to run when the subscription is applied and the matching rows are inserted into the client cache. The [`EventContext`](#type-eventcontext) passed to the callback will have `Event::SubscribeApplied` as its `event`. #### Method `subscribe` diff --git a/docs/sdks/typescript/index.md b/docs/sdks/typescript/index.md index 4f4e17da..34d9edef 100644 --- a/docs/sdks/typescript/index.md +++ b/docs/sdks/typescript/index.md @@ -471,7 +471,7 @@ Identity.fromString(str: string): Identity ### Class `Address` -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#type-identity). +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity). Defined in [spacetimedb-sdk.address](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/address.ts): @@ -561,9 +561,8 @@ The generated class has a field for each of the table's columns, whose names are | Properties | Description | | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | [`Table.name`](#table-name) | The name of the class. | -| [`Table.tableName`](#table-tableName) | The name of the table in the database. | +| [`Table.tableName`](#table-tablename) | The name of the table in the database. | | Methods | | -| [`Table.isEqual`](#table-isequal) | Method to compare two identities. | | [`Table.all`](#table-all) | Return all the subscribed rows in the table. | | [`Table.filterBy{COLUMN}`](#table-filterbycolumn) | Autogenerated; return subscribed rows with a given value in a particular column. `{COLUMN}` is a placeholder for a column name. | | [`Table.findBy{COLUMN}`](#table-findbycolumn) | Autogenerated; return a subscribed row with a given value in a particular unique column. `{COLUMN}` is a placeholder for a column name. | @@ -857,7 +856,7 @@ Person.onUpdate((oldPerson, newPerson, reducerEvent) => { ### {Table} removeOnUpdate -Unregister a previously-registered [`onUpdate`](#table-onUpdate) callback. +Unregister a previously-registered [`onUpdate`](#table-onupdate) callback. ```ts {Table}.removeOnUpdate(callback: (oldValue: {Table}, newValue: {Table}, reducerEvent: ReducerEvent | undefined) => void): void @@ -912,7 +911,7 @@ Person.onDelete((person, reducerEvent) => { ### {Table} removeOnDelete -Unregister a previously-registered [`onDelete`](#table-onDelete) callback. +Unregister a previously-registered [`onDelete`](#table-ondelete) callback. ```ts {Table}.removeOnDelete(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void