mirror of
https://github.com/XTLS/Xray-docs-next.git
synced 2026-09-25 15:58:17 +03:00
95 lines
2.9 KiB
JavaScript
95 lines
2.9 KiB
JavaScript
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}`);
|