1- // ---------------------------------------------------------------------------
2- // GitHub API utilities
3- //
4- // Data is fetched at most once every CACHE_TTL_MS (24 h) via a module-level
5- // in-memory cache. This means:
6- // • SSG — cache is cold on every `astro build`, so data is always fresh.
7- // • SSR — the cache is shared across requests within the same server
8- // process and expires after 24 h, keeping API usage minimal.
9- //
10- // GITHUB_TOKEN is provided through Astro's server-only env API and is never
11- // bundled into any client-side output.
12- // ---------------------------------------------------------------------------
13-
14- import { GITHUB_TOKEN } from 'astro:env/server' ;
1+ import repoStatsSnapshot from '@/data/github-snapshot' ;
152
163export const REPO_OWNER = 'bubbuild' ;
174export const REPO_NAME = 'bub' ;
185export const REPO_SLUG = `${ REPO_OWNER } /${ REPO_NAME } ` ;
196export const REPO_URL = `https://github.com/${ REPO_SLUG } ` ;
207export const CONTRIBUTORS_URL = `${ REPO_URL } /graphs/contributors` ;
21- const API_BASE = 'https://api.github.com' ;
22-
23- /** How long (ms) to keep cached data before re-fetching. Default: 24 h. */
24- const CACHE_TTL_MS = 24 * 60 * 60 * 1_000 ;
258
269export interface GitHubContributor {
2710 login : string ;
@@ -32,106 +15,15 @@ export interface GitHubContributor {
3215}
3316
3417export interface RepoStats {
35- /** Raw star count (0 when unavailable). */
3618 stars : number ;
37- /** Formatted star string, e.g. "1.2k" (undefined when stars === 0). */
3819 starsFormatted : string | undefined ;
39- /** Top contributors ordered by contribution count. */
4020 contributors : GitHubContributor [ ] ;
4121}
4222
43- // ---------------------------------------------------------------------------
44- // Module-level cache — survives across requests in SSR; cold on each build.
45- // ---------------------------------------------------------------------------
46- interface Cache {
47- stats : RepoStats ;
48- fetchedAt : number ;
49- }
50-
51- let _cache : Cache | null = null ;
52-
53- // ---------------------------------------------------------------------------
54- // Internal helpers
55- // ---------------------------------------------------------------------------
56-
57- function buildHeaders ( ) : HeadersInit {
58- return {
59- Accept : 'application/vnd.github+json' ,
60- 'X-GitHub-Api-Version' : '2022-11-28' ,
61- ...( GITHUB_TOKEN ? { Authorization : `Bearer ${ GITHUB_TOKEN } ` } : { } ) ,
62- } ;
63- }
64-
65- /** Format a raw star count into a human-readable string (e.g. 1234 → "1.2k"). */
66- export function formatStars ( count : number ) : string | undefined {
67- if ( count <= 0 ) return undefined ;
68- if ( count >= 1_000 ) {
69- return `${ ( count / 1_000 ) . toFixed ( 1 ) . replace ( / \. 0 $ / , '' ) } k` ;
70- }
71- return String ( count ) ;
72- }
73-
74- async function fetchFromAPI ( contributorLimit : number ) : Promise < RepoStats > {
75- const headers = buildHeaders ( ) ;
76-
77- const [ repoRes , contribRes ] = await Promise . allSettled ( [
78- fetch ( `${ API_BASE } /repos/${ REPO_OWNER } /${ REPO_NAME } ` , { headers } ) ,
79- fetch (
80- // Fetch extra entries so bots filtered out still leave `contributorLimit` humans.
81- `${ API_BASE } /repos/${ REPO_OWNER } /${ REPO_NAME } /contributors?per_page=${ contributorLimit + 10 } &anon=false` ,
82- { headers } ,
83- ) ,
84- ] ) ;
85-
86- let stars = 0 ;
87- if ( repoRes . status === 'fulfilled' && repoRes . value . ok ) {
88- const data = await repoRes . value . json ( ) as { stargazers_count : number } ;
89- stars = data . stargazers_count ?? 0 ;
90- }
91-
92- let contributors : GitHubContributor [ ] = [ ] ;
93- if ( contribRes . status === 'fulfilled' && contribRes . value . ok ) {
94- const data = await contribRes . value . json ( ) as GitHubContributor [ ] ;
95- contributors = Array . isArray ( data )
96- ? data . filter ( ( c ) => c . type === 'User' ) . slice ( 0 , contributorLimit )
97- : [ ] ;
98- }
99-
100- return { stars, starsFormatted : formatStars ( stars ) , contributors } ;
101- }
102-
103- // ---------------------------------------------------------------------------
104- // Public API
105- // ---------------------------------------------------------------------------
106-
107- /**
108- * Return cached repo stats, refreshing when the cache is cold or stale.
109- *
110- * @param contributorLimit Max number of contributors to return (default 15).
111- */
112- export async function getRepoStats ( contributorLimit = 15 ) : Promise < RepoStats > {
113- const now = Date . now ( ) ;
114- if ( _cache && now - _cache . fetchedAt < CACHE_TTL_MS ) {
115- return _cache . stats ;
116- }
117-
118- try {
119- const stats = await fetchFromAPI ( contributorLimit ) ;
120- _cache = { stats, fetchedAt : now } ;
121- return stats ;
122- } catch {
123- // On total failure, return stale cache if available, else empty defaults.
124- if ( _cache ) return _cache . stats ;
125- return { stars : 0 , starsFormatted : undefined , contributors : [ ] } ;
126- }
23+ export function getRepoStats ( ) : RepoStats {
24+ return repoStatsSnapshot ;
12725}
12826
129- /**
130- * Build a GitHub avatar URL for a username.
131- *
132- * Uses GitHub's redirect-based endpoint — no API call, no token, no rate limit.
133- * Returns `undefined` when no username is provided.
134- */
13527export function gitHubAvatarUrl ( username : string | undefined , size = 80 ) : string | undefined {
13628 return username ? `https://github.com/${ username } .png?size=${ size } ` : undefined ;
13729}
0 commit comments