Skip to content

Commit 3664c20

Browse files
chore(website): account for thanks.dev donors in sponsors list (typescript-eslint#10172)
* clean up * finish * extract extra variables * add knip ignore * remove unnecessary nullish coalesce * Update tools/scripts/generate-sponsors.mts --------- Co-authored-by: Josh Goldberg ✨ <git@joshuakgoldberg.com>
1 parent f034c17 commit 3664c20

File tree

5 files changed

+161
-76
lines changed

5 files changed

+161
-76
lines changed

knip.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export default {
2626
'make-dir',
2727
'ncp',
2828
'tmp',
29+
// imported for type purposes only
30+
'website',
2931
],
3032
entry: ['tools/release/changelog-renderer.js', 'tools/scripts/**/*.mts'],
3133
},

packages/website/data/sponsors.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,27 @@
132132
"totalDonations": 70000,
133133
"website": "https://www.codiga.io"
134134
},
135+
{
136+
"id": "frontendmasters",
137+
"image": "https://avatars.githubusercontent.com/u/5613852?v=4",
138+
"name": "Frontend Masters",
139+
"totalDonations": 63327,
140+
"website": "https://FrontendMasters.com"
141+
},
135142
{
136143
"id": "DeepSource",
137144
"image": "https://images.opencollective.com/deepsource/0f18cea/logo.png",
138145
"name": "DeepSource",
139146
"totalDonations": 60000,
140147
"website": "https://deepsource.io/"
141148
},
149+
{
150+
"id": "syntaxfm",
151+
"image": "https://avatars.githubusercontent.com/u/130389858?v=4",
152+
"name": "Syntax",
153+
"totalDonations": 54449,
154+
"website": "https://syntax.fm"
155+
},
142156
{
143157
"id": "Future Processing",
144158
"image": "https://images.opencollective.com/future-processing/1410d26/logo.png",
@@ -293,6 +307,13 @@
293307
"totalDonations": 17000,
294308
"website": "https://balsa.com/"
295309
},
310+
{
311+
"id": "codecov",
312+
"image": "https://avatars.githubusercontent.com/u/8226205?v=4",
313+
"name": "Codecov",
314+
"totalDonations": 15292,
315+
"website": "https://codecov.io/"
316+
},
296317
{
297318
"id": "THE PADDING",
298319
"image": "https://images.opencollective.com/thepadding/55e79ad/logo.png",
@@ -314,6 +335,13 @@
314335
"totalDonations": 14500,
315336
"website": "https://now4real.com/"
316337
},
338+
{
339+
"id": "getsentry",
340+
"image": "https://avatars.githubusercontent.com/u/1396951?v=4",
341+
"name": "Sentry",
342+
"totalDonations": 14436,
343+
"website": "https://sentry.io"
344+
},
317345
{
318346
"id": "Knowledge Work",
319347
"image": "https://images.opencollective.com/knowledge-work/f91b72d/logo.png",

packages/website/src/components/FinancialContributors/Sponsor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function Sponsor({
2727
return (
2828
<Link
2929
className={styles.sponsorLink}
30-
href={sponsor.website ?? undefined}
30+
href={sponsor.website}
3131
title={sponsor.name}
3232
rel="noopener sponsored"
3333
>
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
export interface SponsorData {
2-
description?: string;
32
id: string;
43
image: string;
54
name: string;
6-
tier?: string;
75
totalDonations: number;
8-
website?: string;
6+
website: string;
97
}

tools/scripts/generate-sponsors.mts

Lines changed: 129 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,98 +2,155 @@ import * as fs from 'node:fs';
22
import * as path from 'node:path';
33

44
import fetch from 'cross-fetch';
5+
import type { SponsorData } from 'website/src/components/FinancialContributors/types.ts';
56

67
import { PACKAGES_WEBSITE } from './paths.mts';
78

8-
type MemberNodes = {
9-
account: {
10-
id: string;
11-
imageUrl: string;
12-
name: string;
13-
website: string;
14-
};
15-
totalDonations: { valueInCents: number };
16-
}[];
17-
189
const excludedNames = new Set([
1910
'Josh Goldberg', // Team member 💖
2011
]);
2112

2213
const filteredTerms = ['casino', 'deepnude', 'tiktok'];
2314

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
5864
}
5965
}
6066
}
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+
});
66108

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+
])
74144
)
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()
88146
.filter(
89-
({ id, name, totalDonations, website }) =>
147+
({ id, name, totalDonations }) =>
90148
!(
91149
filteredTerms.some(filteredTerm =>
92150
name.toLowerCase().includes(filteredTerm),
93151
) ||
94152
excludedNames.has(id) ||
95-
totalDonations < 10000 ||
96-
!website
153+
totalDonations < 10000
97154
),
98155
)
99156
.sort((a, b) => b.totalDonations - a.totalDonations);

0 commit comments

Comments
 (0)