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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy