import { execSync } from "node:child_process" import fs from "node:fs" import path from "node:path" import crypto from "node:crypto" const ROOT = process.cwd() const DOCS_DIR = path.resolve(ROOT, "docs") const OUT_DIR = path.resolve(ROOT, ".vitepress/.generated") const OUT_FILE = path.join(OUT_DIR, "contributors.json") function walk(dir) { const out = [] for (const name of fs.readdirSync(dir)) { const p = path.join(dir, name) const st = fs.statSync(p) if (st.isDirectory()) out.push(...walk(p)) else if (p.endsWith(".md")) out.push(p) } return out } function md5(s) { return crypto .createHash("md5") .update(s.trim().toLowerCase()) .digest("hex") } function parseGithubUsernameFromNoreply(email = "") { // 1) 12345+username@users.noreply.github.com // 2) username@users.noreply.github.com const m1 = email.match(/^[^+]+\+([^@]+)@users\.noreply\.github\.com$/i) if (m1?.[1]) return m1[1] const m2 = email.match(/^([^@]+)@users\.noreply\.github\.com$/i) if (m2?.[1]) return m2[1] return null } function avatarUrlFor(email = "") { const gh = parseGithubUsernameFromNoreply(email) if (gh) { return `https://unavatar.io/github/${encodeURIComponent(gh)}` } if (email) { return `https://www.gravatar.com/avatar/${md5(email)}?d=identicon&s=64` } return `https://www.gravatar.com/avatar/?d=identicon&s=64` } function gitContributors(fileAbsPath) { const rel = path .relative(process.cwd(), fileAbsPath) .replaceAll("\\", "/") let raw = "" try { raw = execSync(`git log --follow --format="%aN|%aE" -- "${rel}"`, { encoding: "utf8" }) } catch { return [] } const map = new Map() raw .split("\n") .map((s) => s.trim()) .filter(Boolean) .forEach((line) => { const [name, email] = line.split("|") const key = (email || name || "").toLowerCase() if (!map.has(key)) { const github = parseGithubUsernameFromNoreply(email) map.set(key, { name, email, github, avatarUrl: avatarUrlFor(email), commits: 0 }) } map.get(key).commits += 1 }) return [...map.values()].sort((a, b) => b.commits - a.commits) } const files = walk(DOCS_DIR) const data = {} for (const abs of files) { const relToDocs = path.relative(DOCS_DIR, abs).replaceAll("\\", "/") let route = "/" + relToDocs.replace(/\.md$/, "") route = route.replace(/\/index$/, "/") // docs/a/index.md -> /a/ data[route] = gitContributors(abs) } fs.mkdirSync(OUT_DIR, { recursive: true }) fs.writeFileSync(OUT_FILE, JSON.stringify(data, null, 2), "utf8") console.log( `Generated contributors for ${files.length} pages -> ${OUT_FILE}` )