github-stats.ts 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. import { productRepo } from './shared';
  2. export interface GitHubStats {
  3. stars: number;
  4. forks: number;
  5. latestVersion: string;
  6. }
  7. // Real, recent numbers used as a fallback when the GitHub API is unavailable
  8. // at build time (offline CI, rate limit). Update periodically.
  9. const FALLBACK: GitHubStats = {
  10. stars: 41500,
  11. forks: 7700,
  12. latestVersion: 'v3.x',
  13. };
  14. const API_BASE = `https://api.github.com/repos/${productRepo.user}/${productRepo.repo}`;
  15. /**
  16. * Fetch live repo stats. Runs both at build time (Node) and in the browser —
  17. * api.github.com sends CORS headers, and the unauthenticated limit (60 req/h
  18. * per client IP) is plenty for one call per landing-page visit.
  19. * Returns null on any failure so callers keep the numbers they already have;
  20. * `latestVersion` is '' when the release lookup alone fails.
  21. */
  22. export async function fetchGitHubStats(init?: RequestInit): Promise<GitHubStats | null> {
  23. try {
  24. const [repoRes, releaseRes] = await Promise.all([
  25. fetch(API_BASE, init),
  26. fetch(`${API_BASE}/releases/latest`, init),
  27. ]);
  28. if (!repoRes.ok) return null;
  29. const repo = (await repoRes.json()) as { stargazers_count?: number; forks_count?: number };
  30. if (typeof repo.stargazers_count !== 'number' || typeof repo.forks_count !== 'number') {
  31. return null;
  32. }
  33. let latestVersion = '';
  34. if (releaseRes.ok) {
  35. const release = (await releaseRes.json()) as { tag_name?: string };
  36. if (release.tag_name) latestVersion = release.tag_name;
  37. }
  38. return { stars: repo.stargazers_count, forks: repo.forks_count, latestVersion };
  39. } catch {
  40. return null;
  41. }
  42. }
  43. /**
  44. * Build-time stats used as the initial render (no layout shift, works without
  45. * JS). The client refreshes them via fetchGitHubStats() after hydration.
  46. * Always resolves; on any error it returns the hardcoded fallback.
  47. */
  48. export async function getGitHubStats(): Promise<GitHubStats> {
  49. const headers: Record<string, string> = {
  50. 'User-Agent': '3x-ui-docs',
  51. Accept: 'application/vnd.github+json',
  52. };
  53. if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
  54. const live = await fetchGitHubStats({ headers, next: { revalidate: 3600 } });
  55. if (!live) return FALLBACK;
  56. return { ...live, latestVersion: live.latestVersion || FALLBACK.latestVersion };
  57. }
  58. /** Compact display, e.g. 41523 -> "41.5k". */
  59. export function formatCount(n: number): string {
  60. if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
  61. return String(n);
  62. }