1- import { setTimeout as sleep } from "timers/promises"
2- import { readFileSync , writeFileSync } from "node:fs"
1+ import { setTimeout as sleep } from "node: timers/promises"
2+ import { readFile , writeFile } from "node:fs/promises "
33import { fileURLToPath , pathToFileURL } from "node:url"
44import { dirname , resolve } from "node:path"
5- import { graphql } from "./generated"
5+
66import { ExecutionResult } from "graphql"
7- import { TypedDocumentString } from "./generated/graphql"
7+
8+ import { graphql } from "./generated/index.ts"
9+ import { TypedDocumentString } from "./generated/graphql.ts"
810
911type RepoRef = `${string } /${string } `
1012
@@ -52,7 +54,9 @@ export const REPO_TO_PROJECT: Record<RepoRef, string> = {
5254 */
5355export async function getContributors (
5456 repoToProject : Record < RepoRef , string > = REPO_TO_PROJECT ,
55- ) : Promise < ExecutionResult < typeof QUERY > > {
57+ ) : Promise <
58+ Record < string , Array < { id : string ; website ?: string ; contributions : number } > >
59+ > {
5660 const accessToken = process . env . GITHUB_ACCESS_TOKEN
5761 if ( ! accessToken ) {
5862 console . warn (
@@ -117,7 +121,10 @@ export async function getContributors(
117121 )
118122
119123 // Convert to the requested output shape and sort by contributions
120- const result : ExecutionResult < typeof QUERY > = { }
124+ const result : Record <
125+ string ,
126+ Array < { id : string ; website ?: string ; contributions : number } >
127+ > = { }
121128 for ( const [ project , map ] of perProject ) {
122129 const arr = Array . from ( map . values ( ) ) . sort (
123130 ( a , b ) => b . contributions - a . contributions ,
@@ -138,23 +145,30 @@ async function fetchRepoContributors(
138145 repo : string ,
139146 accessToken : string ,
140147) {
141- // login -> { contributions, website }
142- const counts = new Map < string , { contributions : number ; website ?: string } > ( )
148+ const contributors = new Map <
149+ string /* handle */ ,
150+ { contributions : number ; website ?: string }
151+ > ( )
143152 let after : string | null = null
144153 let page = 0
145154 let hasMore = true
146155
147- while ( hasMore ) {
148- const response = await execute ( QUERY , {
149- owner,
150- name : repo ,
151- after,
152- } , {
153- headers : {
156+ const fetchMore = ( ) =>
157+ execute (
158+ QUERY ,
159+ {
160+ owner,
161+ name : repo ,
162+ after,
163+ } ,
164+ {
154165 Authorization : `Bearer ${ accessToken } ` ,
155- ' User-Agent' : ' graphql.org contributors sync client' ,
166+ " User-Agent" : " graphql.org contributors sync client" ,
156167 } ,
157- } )
168+ )
169+
170+ while ( hasMore ) {
171+ const response = await fetchMore ( )
158172
159173 if ( response . errors ?. length ) {
160174 throw new Error (
@@ -164,55 +178,58 @@ async function fetchRepoContributors(
164178 )
165179 }
166180
167- const repoDataRaw = response . data ?[ 0 ] .
168- if ( ! repoDataRaw ) {
181+ const repoData = response . data ?. repository
182+ if ( ! repoData ) {
169183 console . warn ( `Repository not found: ${ owner } /${ repo } ` )
170184 break
171185 }
172- const repoData = repoDataRaw as NonNullable < RepoHistoryPage [ "repository" ] >
173186
174187 const defaultBranchRef = repoData . defaultBranchRef
175- if ( ! defaultBranchRef || ! defaultBranchRef . target ) {
188+ if ( ! defaultBranchRef ? .target ) {
176189 console . warn ( `Default branch not found for ${ owner } /${ repo } ` )
177190 break
178191 }
192+
193+ if ( defaultBranchRef . target . __typename !== "Commit" ) {
194+ throw new Error ( `Invalid typename for ${ owner } /${ repo } ` )
195+ }
196+
179197 const history = defaultBranchRef . target . history
180198
181- for ( const node of history . nodes ) {
182- const user = node . author ?. user
199+ for ( const node of history . nodes || [ ] ) {
200+ const user = node ? .author ?. user
183201 if ( ! user ?. login ) continue
184- const prev = counts . get ( user . login )
202+ const prev = contributors . get ( user . login )
185203 if ( prev ) {
186204 prev . contributions += 1
187205 // keep existing website unless we don't have one and GitHub provides it
188- if ( ! prev . website && user . websiteUrl ) prev . website = user . websiteUrl
206+ prev . website || = user . websiteUrl
189207 } else {
190- counts . set ( user . login , {
208+ contributors . set ( user . login , {
191209 contributions : 1 ,
192210 website : user . websiteUrl ?? undefined ,
193211 } )
194212 }
195213 }
196214
197- const hasNext = history . pageInfo . hasNextPage
198- after = history . pageInfo . endCursor
199- hasMore = hasNext
215+ const hasNext = history . pageInfo ? .hasNextPage
216+ after = history . pageInfo ? .endCursor || null
217+ hasMore = ! ! hasNext
200218 page += 1
201219
202- // Brief backoff every few pages to be nicer to the API
203220 if ( page % 5 === 0 ) await sleep ( 200 )
204221 }
205222
206- return counts
223+ return contributors
207224}
208225
209226// CLI entrypoint: when executed directly, write contributors to data.json next to this file
210227if ( import . meta. url === pathToFileURL ( process . argv [ 1 ] ?? "" ) . href ) {
211228 const __dirname = dirname ( fileURLToPath ( import . meta. url ) )
212229 const outPath = resolve ( __dirname , "data.json" )
213230 getContributors ( )
214- . then ( data => {
215- writeFileSync ( outPath , JSON . stringify ( data , null , 2 ) + "\n" , "utf8" )
231+ . then ( async data => {
232+ await writeFile ( outPath , JSON . stringify ( data , null , 2 ) + "\n" , "utf8" )
216233 console . log (
217234 `Wrote ${ Object . values ( data ) . reduce ( ( n , arr ) => n + arr . length , 0 ) } contributors across ${ Object . keys ( data ) . length } projects to ${ outPath } ` ,
218235 )
@@ -225,9 +242,9 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
225242
226243async function execute < TResult , TVariables > (
227244 query : TypedDocumentString < TResult , TVariables > ,
228- headers ?: Record < string , string > ,
229245 variables ?: TVariables ,
230- ) {
246+ headers ?: Record < string , string > ,
247+ ) : Promise < ExecutionResult < TResult > > {
231248 const response = await fetch ( "https://graphql.org/graphql/" , {
232249 method : "POST" ,
233250 headers : {
@@ -245,5 +262,5 @@ async function execute<TResult, TVariables>(
245262 throw new Error ( "Network response was not ok" )
246263 }
247264
248- return response . json ( ) as ExecutionResult < TResult >
265+ return ( await response . json ( ) ) as ExecutionResult < TResult >
249266}
0 commit comments