@@ -2,98 +2,155 @@ import * as fs from 'node:fs';
2
2
import * as path from 'node:path' ;
3
3
4
4
import fetch from 'cross-fetch' ;
5
+ import type { SponsorData } from 'website/src/components/FinancialContributors/types.ts' ;
5
6
6
7
import { PACKAGES_WEBSITE } from './paths.mts' ;
7
8
8
- type MemberNodes = {
9
- account : {
10
- id : string ;
11
- imageUrl : string ;
12
- name : string ;
13
- website : string ;
14
- } ;
15
- totalDonations : { valueInCents : number } ;
16
- } [ ] ;
17
-
18
9
const excludedNames = new Set ( [
19
10
'Josh Goldberg' , // Team member 💖
20
11
] ) ;
21
12
22
13
const filteredTerms = [ 'casino' , 'deepnude' , 'tiktok' ] ;
23
14
24
- const { members } = (
25
- ( await (
26
- await fetch ( 'https://api.opencollective.com/graphql/v2' , {
27
- method : 'POST' ,
28
- headers : { 'Content-Type' : 'application/json' } ,
29
- body : JSON . stringify ( {
30
- query : `
31
- {
32
- collective(slug: "typescript-eslint") {
33
- members(limit: 1000, role: BACKER) {
34
- nodes {
35
- account {
36
- id
37
- imageUrl
38
- name
39
- website
40
- }
41
- tier {
42
- amount {
43
- valueInCents
44
- }
45
- orders(limit: 100) {
46
- nodes {
47
- amount {
48
- valueInCents
49
- }
50
- }
51
- }
52
- }
53
- totalDonations {
54
- valueInCents
55
- }
56
- updatedAt
57
- }
15
+ const jsonApiFetch = async < T , > (
16
+ api : string ,
17
+ options ?: RequestInit ,
18
+ ) : Promise < T > => {
19
+ const url = `https://api.${ api } ` ;
20
+ const response = await fetch ( url , options ) ;
21
+ if ( ! response . ok ) {
22
+ console . error ( {
23
+ url,
24
+ response : { status : response . status , body : await response . text ( ) } ,
25
+ } ) ;
26
+ throw new Error ( 'API call failed.' ) ;
27
+ }
28
+ return ( await response . json ( ) ) as T ;
29
+ } ;
30
+
31
+ const openCollectiveSponsorsPromise = jsonApiFetch < {
32
+ data : {
33
+ collective : {
34
+ members : {
35
+ nodes : {
36
+ account : {
37
+ id : string ;
38
+ imageUrl : string ;
39
+ name : string ;
40
+ website : string | null ;
41
+ } ;
42
+ totalDonations : { valueInCents : number } ;
43
+ } [ ] ;
44
+ } ;
45
+ } ;
46
+ } ;
47
+ } > ( 'opencollective.com/graphql/v2' , {
48
+ method : 'POST' ,
49
+ headers : { 'Content-Type' : 'application/json' } ,
50
+ body : JSON . stringify ( {
51
+ query : `
52
+ {
53
+ collective(slug: "typescript-eslint") {
54
+ members(limit: 1000, role: BACKER) {
55
+ nodes {
56
+ account {
57
+ id
58
+ imageUrl
59
+ name
60
+ website
61
+ }
62
+ totalDonations {
63
+ valueInCents
58
64
}
59
65
}
60
66
}
61
- ` ,
62
- } ) ,
63
- } )
64
- ) . json ( ) ) as { data : { collective : { members : { nodes : MemberNodes } } } }
65
- ) . data . collective ;
67
+ }
68
+ }
69
+ ` ,
70
+ } ) ,
71
+ } ) . then ( ( { data } ) => {
72
+ // TODO: remove polyfill in Node 22
73
+ const groupBy = < T , > (
74
+ arr : T [ ] ,
75
+ fn : ( item : T ) => string ,
76
+ ) : Record < string , T [ ] > => {
77
+ const grouped : Record < string , T [ ] > = { } ;
78
+ for ( const item of arr ) {
79
+ ( grouped [ fn ( item ) ] ??= [ ] ) . push ( item ) ;
80
+ }
81
+ return grouped ;
82
+ } ;
83
+ return Object . entries (
84
+ groupBy (
85
+ data . collective . members . nodes ,
86
+ ( { account } ) => account . name || account . id ,
87
+ ) ,
88
+ ) . flatMap ( ( [ id , members ] ) => {
89
+ const [
90
+ {
91
+ account : { website, ...account } ,
92
+ } ,
93
+ ] = members ;
94
+ return website
95
+ ? {
96
+ id,
97
+ image : account . imageUrl ,
98
+ name : account . name ,
99
+ totalDonations : members . reduce (
100
+ ( sum , { totalDonations } ) => sum + totalDonations . valueInCents ,
101
+ 0 ,
102
+ ) ,
103
+ website,
104
+ }
105
+ : [ ] ;
106
+ } ) ;
107
+ } ) ;
66
108
67
- const sponsors = Object . entries (
68
- // TODO: use Object.groupBy in Node 22
69
- members . nodes . reduce < Record < string , MemberNodes > > ( ( membersById , member ) => {
70
- const { account } = member ;
71
- ( membersById [ account . name || account . id ] ??= [ ] ) . push ( member ) ;
72
- return membersById ;
73
- } , { } ) ,
109
+ const thanksDevSponsorsPromise = jsonApiFetch <
110
+ Record < 'dependers' | 'donors' , [ 'gh' | 'gl' , string , number ] [ ] >
111
+ > ( 'thanks.dev/v1/vip/dependee/gh/typescript-eslint' ) . then ( async ( { donors } ) =>
112
+ (
113
+ await Promise . all (
114
+ donors
115
+ /* GitLab does not have an API to get a user's profile. At the time of writing, only 13% of donors
116
+ from thanks.dev came from GitLab rather than GitHub, and none of them met the contribution
117
+ threshold. */
118
+ . filter ( ( [ site ] ) => site === 'gh' )
119
+ . map ( async ( [ , id , totalDonations ] ) => {
120
+ const { name, ...github } = await jsonApiFetch <
121
+ Record < 'avatar_url' | 'blog' , string > & {
122
+ name : string | null ;
123
+ }
124
+ > ( `github.com/users/${ id } ` ) ;
125
+ return name
126
+ ? {
127
+ id,
128
+ image : github . avatar_url ,
129
+ name,
130
+ totalDonations,
131
+ website : github . blog || `https://github.com/${ id } ` ,
132
+ }
133
+ : [ ] ;
134
+ } ) ,
135
+ )
136
+ ) . flat ( ) ,
137
+ ) ;
138
+
139
+ const sponsors = (
140
+ await Promise . all < SponsorData [ ] > ( [
141
+ openCollectiveSponsorsPromise ,
142
+ thanksDevSponsorsPromise ,
143
+ ] )
74
144
)
75
- . map ( ( [ id , members ] ) => {
76
- const [ { account } ] = members ;
77
- return {
78
- id,
79
- image : account . imageUrl ,
80
- name : account . name ,
81
- totalDonations : members . reduce (
82
- ( sum , { totalDonations } ) => sum + totalDonations . valueInCents ,
83
- 0 ,
84
- ) ,
85
- website : account . website ,
86
- } ;
87
- } )
145
+ . flat ( )
88
146
. filter (
89
- ( { id, name, totalDonations, website } ) =>
147
+ ( { id, name, totalDonations } ) =>
90
148
! (
91
149
filteredTerms . some ( filteredTerm =>
92
150
name . toLowerCase ( ) . includes ( filteredTerm ) ,
93
151
) ||
94
152
excludedNames . has ( id ) ||
95
- totalDonations < 10000 ||
96
- ! website
153
+ totalDonations < 10000
97
154
) ,
98
155
)
99
156
. sort ( ( a , b ) => b . totalDonations - a . totalDonations ) ;
0 commit comments