@@ -7,27 +7,130 @@ import { fileURLToPath } from "node:url";
77const __filename = fileURLToPath ( import . meta. url ) ;
88const __dirname = path . dirname ( __filename ) ;
99
10- const sourceFile = path . join ( __dirname , ".." , ".." , "data" , "repository_stats.json" ) ;
1110const targetFile = path . join ( __dirname , ".." , "data" , "repository_stats.json" ) ;
11+ const fallbackFile = path . join ( __dirname , ".." , ".." , "data" , "repository_stats.json" ) ;
1212
13- function syncRepoStats ( ) {
14- if ( ! fs . existsSync ( sourceFile ) ) {
15- console . warn ( "[sync-repo-stats] Source file not found, skipping sync:" , sourceFile ) ;
16- return ;
13+ const repos = [
14+ "hiero-consensus-node" , "hiero-local-node" , "hiero-mirror-node" ,
15+ "hiero-improvement-proposals" , "hiero-sdk-js" , "hiero-sdk-java" ,
16+ "hiero-json-rpc-relay" , "hiero-sdk-go" , "hiero-sdk-rust" ,
17+ "hiero-mirror-node-explorer" , "hiero-cli" , "solo" , "hiero-block-node" ,
18+ "hiero-sdk-tck" , "hiero-sdk-cpp" , "governance" , "hiero-sdk-python" ,
19+ "hiero-sdk-swift" , "sdk-collaboration-hub" , "tsc" ,
20+ ] ;
21+
22+ async function fetchFromGitHub ( ) {
23+ const stats = new Map ( ) ;
24+ const cachedStats = loadFallback ( false ) ;
25+ const headers = {
26+ "User-Agent" : "hiero-website-build" ,
27+ "Accept" : "application/vnd.github+json" ,
28+ "X-GitHub-Api-Version" : "2022-11-28" ,
29+ } ;
30+ if ( process . env . GITHUB_TOKEN ) {
31+ headers [ "Authorization" ] = `Bearer ${ process . env . GITHUB_TOKEN } ` ;
32+ }
33+
34+ console . log ( "[sync-repo-stats] Fetching repository statistics from GitHub..." ) ;
35+
36+ let successCount = 0 ;
37+
38+ for ( const repo of repos ) {
39+ const response = await fetch ( `https://api.github.com/repos/hiero-ledger/${ repo } ` , { headers } ) ;
40+ if ( response . ok ) {
41+ const data = await response . json ( ) ;
42+ stats . set ( repo , { stars : data . stargazers_count } ) ;
43+ successCount += 1 ;
44+ console . log ( ` ✓ ${ repo } : ${ data . stargazers_count } stars` ) ;
45+ } else {
46+ const cachedStars = cachedStats ?. [ repo ] ?. stars ;
47+ if ( typeof cachedStars === "number" ) {
48+ stats . set ( repo , { stars : cachedStars } ) ;
49+ console . warn ( ` ⚠ ${ repo } : API ${ response . status } , using cached value ${ cachedStars } ` ) ;
50+ } else {
51+ stats . set ( repo , { stars : 0 } ) ;
52+ console . warn ( ` ✗ ${ repo } : API responded with ${ response . status } ` ) ;
53+ }
54+ }
55+
56+ // If access is blocked/rate-limited and nothing has succeeded, stop early.
57+ if ( ( response . status === 401 || response . status === 403 ) && successCount === 0 ) {
58+ const resetAt = response . headers . get ( "x-ratelimit-reset" ) ;
59+ if ( resetAt ) {
60+ const resetTime = new Date ( Number ( resetAt ) * 1000 ) . toISOString ( ) ;
61+ console . warn ( `[sync-repo-stats] GitHub access currently limited; rate limit resets at ${ resetTime } .` ) ;
62+ }
63+ break ;
64+ }
65+
66+ // Small delay to stay well within GitHub's rate limits.
67+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
1768 }
1869
19- const sourceJson = fs . readFileSync ( sourceFile , "utf8" ) ;
70+ // Ensure all repos exist in output, preferring cache over zeroes when available.
71+ for ( const repo of repos ) {
72+ if ( stats . has ( repo ) ) continue ;
73+ const cachedStars = cachedStats ?. [ repo ] ?. stars ;
74+ stats . set ( repo , { stars : typeof cachedStars === "number" ? cachedStars : 0 } ) ;
75+ }
2076
21- // Validate JSON before writing, so bad source data does not break the app silently.
22- JSON . parse ( sourceJson ) ;
77+ return Object . fromEntries ( stats ) ;
78+ }
2379
24- fs . writeFileSync ( targetFile , sourceJson ) ;
25- console . log ( "[sync-repo- stats] Synced repository_stats.json to nextjs/data" ) ;
80+ function totalStars ( stats ) {
81+ return Object . values ( stats ) . reduce ( ( sum , repo ) => sum + ( repo ?. stars ?? 0 ) , 0 ) ;
2682}
2783
28- try {
29- syncRepoStats ( ) ;
30- } catch ( error ) {
31- console . error ( "[sync-repo-stats] Failed:" , error ) ;
32- process . exit ( 1 ) ;
84+ function loadFallback ( log = true ) {
85+ let bestStats = null ;
86+ let bestSource = null ;
87+
88+ for ( const file of [ targetFile , fallbackFile ] ) {
89+ if ( fs . existsSync ( file ) ) {
90+ try {
91+ const parsed = JSON . parse ( fs . readFileSync ( file , "utf8" ) ) ;
92+ if ( ! bestStats || totalStars ( parsed ) > totalStars ( bestStats ) ) {
93+ bestStats = parsed ;
94+ bestSource = file ;
95+ }
96+ } catch {
97+ // Ignore malformed cache files and keep searching.
98+ }
99+ }
100+ }
101+
102+ if ( bestStats ) {
103+ if ( log ) {
104+ console . warn ( `[sync-repo-stats] Using cached data from ${ bestSource } ` ) ;
105+ }
106+ return bestStats ;
107+ }
108+
109+ if ( log ) {
110+ console . warn ( "[sync-repo-stats] No cached data available — writing empty stats." ) ;
111+ }
112+ return Object . fromEntries ( repos . map ( ( r ) => [ r , { stars : 0 } ] ) ) ;
113+ }
114+
115+ async function run ( ) {
116+ let stats ;
117+ try {
118+ stats = await fetchFromGitHub ( ) ;
119+ } catch ( error ) {
120+ console . warn ( `[sync-repo-stats] GitHub fetch failed (${ error . message } ), falling back to cached data.` ) ;
121+ stats = loadFallback ( ) ;
122+ }
123+
124+ const dataDir = path . dirname ( targetFile ) ;
125+ if ( ! fs . existsSync ( dataDir ) ) fs . mkdirSync ( dataDir , { recursive : true } ) ;
126+
127+ fs . writeFileSync ( targetFile , JSON . stringify ( stats , null , 2 ) ) ;
128+
129+ const totalStars = Object . values ( stats ) . reduce ( ( sum , r ) => sum + r . stars , 0 ) ;
130+ console . log ( `[sync-repo-stats] Done. Total stars: ${ totalStars . toLocaleString ( ) } ` ) ;
33131}
132+
133+ run ( ) . catch ( ( error ) => {
134+ console . error ( "[sync-repo-stats] Unexpected error:" , error ) ;
135+ process . exit ( 1 ) ;
136+ } ) ;
0 commit comments