import { describe, test, expect } from "vitest"; import { existsSync, readdirSync, readFileSync } from "fs"; import { join, basename } from "path"; const docsDir = join(import.meta.dirname, "..", "..", "docs", "src", "content", "docs"); const docsSource = join(import.meta.dirname, "docs.ts"); const pagesDir = join(import.meta.dirname, "..", "..", "docs", "pages"); /** Authored prose lives in docs/pages/ (chant #1731); docs.ts keeps only the overview strings. */ function authoredSources(): Array<{ name: string; path: string }> { const sources = [{ name: "docs.ts", path: docsSource }]; if (existsSync(pagesDir)) { for (const file of readdirSync(pagesDir).sort()) { if (file.endsWith(".mdx")) sources.push({ name: `docs/pages/${file}`, path: join(pagesDir, file) }); } } return sources; } const docsExist = existsSync(docsDir); /** * Collect all page slugs from the generated docs directory. */ function getPageSlugs(): Set { if (!docsExist) return new Set(); const slugs = new Set(); for (const file of readdirSync(docsDir)) { if (file.endsWith(".mdx")) { slugs.add(basename(file, ".mdx")); } } return slugs; } /** * Extract markdown links: [text](href) */ function extractMarkdownLinks(content: string): Array<{ text: string; href: string; line: number }> { const links: Array<{ text: string; href: string; line: number }> = []; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const regex = /\[([^\]]+)\]\(([^)]+)\)/g; let match; while ((match = regex.exec(lines[i])) !== null) { const href = match[2]; if (href.startsWith("http://") || href.startsWith("https://")) continue; if (href.startsWith("#")) continue; links.push({ text: match[1], href, line: i + 1 }); } } return links; } /** * Check if a relative link target exists as a page slug. */ function resolveTarget(href: string, slugs: Set): string | null { const pathPart = href.split("#")[0].replace(/\/$/, ""); if (!pathPart) return null; let target: string | undefined; if (pathPart.startsWith("./")) target = pathPart.slice(2); else if (pathPart.startsWith("../")) target = pathPart.slice(3); else if (pathPart.startsWith("/chant/lexicons/aws/")) { target = pathPart.replace("/chant/lexicons/aws/", "").replace(/\/$/, "") || "index"; } else if (!pathPart.includes("/") && !pathPart.startsWith(".")) { target = pathPart; } if (target === undefined) return null; return slugs.has(target) ? null : `target page "${target}" does not exist`; } describe("docs internal links", () => { const slugs = getPageSlugs(); test("page slugs are discovered", () => { if (!docsExist) return; // generated docs not present (e.g. CI) expect(slugs.size).toBeGreaterThan(5); expect(slugs.has("composites")).toBe(true); expect(slugs.has("nested-stacks")).toBe(true); expect(slugs.has("index")).toBe(true); }); // Validate generated MDX files (skip if docs not generated) for (const file of (docsExist ? readdirSync(docsDir) : [])) { if (!file.endsWith(".mdx")) continue; const slug = basename(file, ".mdx"); test(`${slug}.mdx — internal links resolve to existing pages`, () => { const content = readFileSync(join(docsDir, file), "utf-8"); const links = extractMarkdownLinks(content); const errors: string[] = []; for (const link of links) { const error = resolveTarget(link.href, slugs); if (error) errors.push(`line ${link.line}: [${link.text}](${link.href}) — ${error}`); } if (errors.length > 0) { throw new Error(`Broken links in ${file}:\n${errors.join("\n")}`); } }); test(`${slug}.mdx — non-index pages use ../ not ./ for cross-page links`, () => { if (slug === "index") return; const content = readFileSync(join(docsDir, file), "utf-8"); const links = extractMarkdownLinks(content); const errors: string[] = []; for (const link of links) { const pathPart = link.href.split("#")[0]; if (pathPart.startsWith("./")) { const target = pathPart.slice(2).replace(/\/$/, ""); if (slugs.has(target)) { errors.push(`line ${link.line}: [${link.text}](${link.href}) — use "../${target}/" instead`); } } } if (errors.length > 0) { throw new Error(`Broken ./ links in non-index page ${file}:\n${errors.join("\n")}`); } }); } // Validate the authored sources — docs.ts and docs/pages/*.mdx — so a // broken link is caught before regeneration. for (const source of authoredSources()) { test(`${source.name} — cross-page links use ../ not ./`, () => { if (!docsExist) return; // needs generated slugs for validation const content = readFileSync(source.path, "utf-8"); const links = extractMarkdownLinks(content); const errors: string[] = []; for (const link of links) { const pathPart = link.href.split("#")[0]; // Authored pages render as non-index pages, so sibling links must use ../ if (pathPart.startsWith("./") && slugs.has(pathPart.slice(2).replace(/\/$/, ""))) { errors.push(`line ${link.line}: [${link.text}](${link.href}) — use "../" prefix for cross-page links`); } } if (errors.length > 0) { throw new Error(`${source.name} has ./ links that will break on non-index pages:\n${errors.join("\n")}`); } }); test(`${source.name} — link targets exist as pages`, () => { if (!docsExist) return; // needs generated slugs for validation const content = readFileSync(source.path, "utf-8"); const links = extractMarkdownLinks(content); const errors: string[] = []; for (const link of links) { const error = resolveTarget(link.href, slugs); if (error) errors.push(`line ${link.line}: [${link.text}](${link.href}) — ${error}`); } if (errors.length > 0) { throw new Error(`${source.name} has links to non-existent pages:\n${errors.join("\n")}`); } }); } });