clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/184-task-notice-migration
lofi / tools / docs_links.ts
docs_links.ts 1.8K
1import { dirname, resolve } from "node:path";2import { fileURLToPath } from "node:url";34const root = fileURLToPath(new URL("../", import.meta.url));5const markdownFiles = [6 resolve(root, "README.md"),7 resolve(root, "CONTRIBUTING.md"),8 resolve(root, "apps/reference/README.md"),9];1011async function collectMarkdown(directory: string): Promise<void> {12 for await (const entry of Deno.readDir(directory)) {13 const path = resolve(directory, entry.name);14 if (entry.isDirectory) await collectMarkdown(path);15 else if (entry.isFile && entry.name.endsWith(".md")) {16 markdownFiles.push(path);17 }18 }19}2021await collectMarkdown(resolve(root, "docs"));2223const broken: string[] = [];24const linkPattern = /!?\[[^\]]*\]\((<[^>]+>|[^\s)]+)(?:\s+[^)]*)?\)/g;25for (const markdown of markdownFiles) {26 const source = await Deno.readTextFile(markdown);27 for (const match of source.matchAll(linkPattern)) {28 const raw = match[1].replace(/^<|>$/g, "");29 if (raw.startsWith("#") || /^[a-z][a-z0-9+.-]*:/i.test(raw)) continue;30 // Site-absolute routes (e.g. /node/docs/app-ticket) point at deployed31 // pages, some assembled at build time; the Docusaurus build validates32 // them via onBrokenLinks, not the repository tree.33 if (raw.startsWith("/")) continue;34 const target = decodeURIComponent(raw.split("#", 1)[0]);35 if (!target) continue;36 try {37 await Deno.stat(resolve(dirname(markdown), target));38 } catch (error) {39 if (!(error instanceof Deno.errors.NotFound)) throw error;40 broken.push(`${markdown.slice(root.length + 1)} -> ${raw}`);41 }42 }43}4445if (broken.length > 0) {46 console.error(47 `Broken local Markdown links:\n${broken.map((link) => `- ${link}`).join("\n")}`,48 );49 Deno.exit(1);50}5152console.log(`Validated local links in ${markdownFiles.length} Markdown files.`);