Files
assistant-ui--assistant-ui/apps/docs/lib/github.ts
T
wehub-resource-sync e30e75b5d4
Code Quality / Oxlint + Oxfmt (push) Waiting to run
Code Quality / Template Sync (push) Waiting to run
Code Quality / Build Changed Packages (push) Waiting to run
Code Quality / Test Changed Packages (push) Waiting to run
Deploy Expo Example / Deploy Production (push) Waiting to run
Deploy Ink Example / Deploy Production (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.12) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Waiting to run
Deploy Shadcn Registry / Deploy Production (push) Waiting to run
Template Metrics / LOC + Bundle Size (push) Waiting to run
Changesets / Create Version PR (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

389 lines
10 KiB
TypeScript

import "server-only";
const REPO = "assistant-ui/assistant-ui";
const API_BASE = `https://api.github.com/repos/${REPO}`;
const HTML_BASE = `https://github.com/${REPO}`;
export const REVALIDATE = {
WARM: 3600,
COOL: 21_600,
} as const;
const USER_AGENT = "assistant-ui-docs";
function ghHeaders(extra?: HeadersInit): HeadersInit {
const h: Record<string, string> = {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": USER_AGENT,
};
if (process.env.GITHUB_TOKEN) {
h.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}
if (extra) {
for (const [k, v] of Object.entries(extra as Record<string, string>)) {
h[k] = v;
}
}
return h;
}
const cacheInit = (
revalidate: number,
): { cache: "no-store" } | { next: { revalidate: number } } =>
revalidate === 0 ? { cache: "no-store" } : { next: { revalidate } };
async function ghFetch(
path: string,
revalidate: number,
init?: RequestInit & { headers?: Record<string, string> },
): Promise<Response> {
const { next: initNext, ...rest } = init ?? {};
const cache = cacheInit(revalidate);
return fetch(`${API_BASE}${path}`, {
...rest,
headers: ghHeaders(init?.headers),
...("next" in cache
? { next: { ...cache.next, ...(initNext ?? {}) } }
: cache),
});
}
function parseLastPage(linkHeader: string | null): number | null {
if (!linkHeader) return null;
const match = linkHeader.match(/<[^>]*[?&]page=(\d+)[^>]*>;\s*rel="last"/);
return match ? Number(match[1]) : null;
}
export type RepoStats = {
stars: number;
forks: number;
openIssues: number;
watchers: number;
};
export async function getRepo(
revalidate: number = REVALIDATE.WARM,
): Promise<RepoStats | null> {
try {
const res = await ghFetch("", revalidate);
if (!res.ok) return null;
const data = await res.json();
if (typeof data?.stargazers_count !== "number") return null;
return {
stars: data.stargazers_count,
forks: data.forks_count,
openIssues: data.open_issues_count,
watchers: data.subscribers_count,
};
} catch {
return null;
}
}
export type GitHubRelease = {
draft: boolean;
prerelease: boolean;
tag_name: string;
body: string | null;
html_url: string;
published_at: string;
created_at: string;
};
export async function getReleases(
maxPages = 5,
revalidate: number = REVALIDATE.WARM,
): Promise<GitHubRelease[]> {
const all: GitHubRelease[] = [];
try {
for (let page = 1; page <= maxPages; page++) {
const res = await ghFetch(
`/releases?per_page=100&page=${page}`,
revalidate,
);
if (!res.ok) break;
const batch = (await res.json()) as GitHubRelease[];
if (batch.length === 0) break;
all.push(...batch);
if (batch.length < 100) break;
}
} catch {}
return all;
}
export type CommitActivityWeek = {
days: number[];
total: number;
week: number;
};
export async function getCommitActivityStats(
revalidate: number = REVALIDATE.COOL,
): Promise<CommitActivityWeek[] | null> {
try {
const res = await ghFetch("/stats/commit_activity", revalidate);
// 202 means GitHub is still computing; caller falls back to commit list.
if (!res.ok || res.status === 202) return null;
const data = (await res.json()) as CommitActivityWeek[];
return Array.isArray(data) && data.length > 0 ? data : null;
} catch {
return null;
}
}
export type CommitListItem = {
commit?: {
message?: string;
author?: { date?: string };
committer?: { date?: string };
};
};
export async function getCommitsSince(
sinceIso: string,
maxPages = 20,
revalidate: number = REVALIDATE.COOL,
): Promise<CommitListItem[]> {
const all: CommitListItem[] = [];
try {
for (let page = 1; page <= maxPages; page++) {
const res = await ghFetch(
`/commits?since=${sinceIso}&per_page=100&page=${page}`,
revalidate,
);
if (!res.ok) break;
const batch = (await res.json()) as CommitListItem[];
if (batch.length === 0) break;
all.push(...batch);
if (batch.length < 100) break;
}
} catch {}
return all;
}
export type CommitStats = {
total: number | null;
firstCommitDate: string | null;
};
const commitDate = (item: CommitListItem | undefined): string | null =>
item?.commit?.author?.date ?? item?.commit?.committer?.date ?? null;
export async function getCommitStats(
revalidate: number = REVALIDATE.COOL,
): Promise<CommitStats> {
try {
const res = await ghFetch("/commits?per_page=1", revalidate);
if (!res.ok) return { total: null, firstCommitDate: null };
// With per_page=1 the last page number equals the total number of commits on the default branch (every author, bots included).
const last = parseLastPage(res.headers.get("Link"));
const page1 = (await res.json()) as CommitListItem[];
const total = last ?? (Array.isArray(page1) ? page1.length : null);
if (last == null || last <= 1) {
return { total, firstCommitDate: commitDate(page1[0]) };
}
const oldest = await ghFetch(
`/commits?per_page=1&page=${last}`,
revalidate,
);
if (!oldest.ok) return { total, firstCommitDate: null };
const data = (await oldest.json()) as CommitListItem[];
return { total, firstCommitDate: commitDate(data[0]) };
} catch {
return { total: null, firstCommitDate: null };
}
}
export type CoAuthor = {
name: string;
email: string;
id: number | null;
login: string | null;
count: number;
};
const CO_AUTHOR_RE = /co-authored-by:\s*([^<\n]+?)\s*<([^>]+)>/gi;
const NOREPLY_RE = /^(?:(\d+)\+)?(.+)@users\.noreply\.github\.com$/i;
const COMMIT_PAGE_CONCURRENCY = 8;
const MAX_COMMIT_PAGES = 60;
export async function getCommitCoAuthors(
revalidate: number = REVALIDATE.COOL,
): Promise<CoAuthor[] | null> {
try {
const first = await ghFetch("/commits?per_page=100&page=1", revalidate);
if (!first.ok) return null;
const lastPage = Math.min(
parseLastPage(first.headers.get("Link")) ?? 1,
MAX_COMMIT_PAGES,
);
const pages: CommitListItem[][] = [
(await first.json()) as CommitListItem[],
];
const rest: number[] = [];
for (let page = 2; page <= lastPage; page++) rest.push(page);
for (let i = 0; i < rest.length; i += COMMIT_PAGE_CONCURRENCY) {
const batch = await Promise.all(
rest.slice(i, i + COMMIT_PAGE_CONCURRENCY).map(async (page) => {
const res = await ghFetch(
`/commits?per_page=100&page=${page}`,
revalidate,
);
return res.ok ? ((await res.json()) as CommitListItem[]) : [];
}),
);
pages.push(...batch);
}
const byEmail = new Map<string, CoAuthor>();
for (const batch of pages) {
for (const item of batch) {
const message = item.commit?.message;
if (!message) continue;
for (const match of message.matchAll(CO_AUTHOR_RE)) {
const name = match[1]!.trim();
const email = match[2]!.trim().toLowerCase();
const existing = byEmail.get(email);
if (existing) {
existing.count++;
} else {
const noreply = email.match(NOREPLY_RE);
byEmail.set(email, {
name,
email,
id: noreply?.[1] ? Number(noreply[1]) : null,
login: noreply ? noreply[2]! : null,
count: 1,
});
}
}
}
}
return Array.from(byEmail.values());
} catch {
return null;
}
}
export type GitHubUser = {
login: string;
type: string;
avatarUrl: string;
htmlUrl: string;
};
async function fetchGitHubUser(
path: string,
revalidate: number,
): Promise<GitHubUser | null> {
try {
const res = await fetch(`https://api.github.com/${path}`, {
headers: ghHeaders(),
...cacheInit(revalidate),
});
if (!res.ok) return null;
const data = await res.json();
if (typeof data?.login !== "string") return null;
return {
login: data.login,
type: data.type,
avatarUrl: data.avatar_url,
htmlUrl: data.html_url,
};
} catch {
return null;
}
}
export const getUser = (login: string, revalidate: number = REVALIDATE.COOL) =>
fetchGitHubUser(`users/${encodeURIComponent(login)}`, revalidate);
export const getUserById = (id: number, revalidate: number = REVALIDATE.COOL) =>
fetchGitHubUser(`user/${id}`, revalidate);
export type GitHubContributor = {
login: string;
avatar_url: string;
html_url: string;
contributions: number;
type?: string;
};
export async function getContributors(
maxPages = 2,
revalidate: number = REVALIDATE.COOL,
): Promise<GitHubContributor[] | null> {
try {
const all: GitHubContributor[] = [];
for (let page = 1; page <= maxPages; page++) {
const res = await ghFetch(
`/contributors?per_page=100&page=${page}`,
revalidate,
);
if (!res.ok) {
if (page === 1) return null;
break;
}
const batch = (await res.json()) as GitHubContributor[];
if (batch.length === 0) break;
all.push(...batch);
if (batch.length < 100) break;
}
return all;
} catch {
return null;
}
}
export type StargazerEntry = { starred_at: string };
export async function getStargazersPage(
page: number,
revalidate: number = REVALIDATE.COOL,
): Promise<{
data: StargazerEntry[];
lastPage: number | null;
}> {
try {
const res = await ghFetch(
`/stargazers?per_page=100&page=${page}`,
revalidate,
{ headers: { Accept: "application/vnd.github.star+json" } },
);
if (!res.ok) return { data: [], lastPage: null };
const data = (await res.json()) as StargazerEntry[];
return { data, lastPage: parseLastPage(res.headers.get("Link")) };
} catch {
return { data: [], lastPage: null };
}
}
export async function getDependents(
revalidate: number = REVALIDATE.COOL,
): Promise<{
repos: number;
packages: number;
} | null> {
try {
const res = await fetch(`${HTML_BASE}/network/dependents`, {
headers: { "User-Agent": "Mozilla/5.0 (assistant-ui-traction)" },
...cacheInit(revalidate),
});
if (!res.ok) return null;
const html = await res.text();
const reposMatch = html.match(/([\d,]+)\s+Repositories\b/);
const packagesMatch = html.match(/([\d,]+)\s+Packages\b/);
if (!reposMatch && !packagesMatch) return null;
return {
repos: reposMatch ? Number(reposMatch[1]!.replace(/,/g, "")) : 0,
packages: packagesMatch ? Number(packagesMatch[1]!.replace(/,/g, "")) : 0,
};
} catch {
return null;
}
}