Skip to content

chore(website): account for thanks.dev donors in sponsors list #10172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export default {
'make-dir',
'ncp',
'tmp',
// imported for type purposes only
'website',
],
entry: ['tools/release/changelog-renderer.js', 'tools/scripts/**/*.mts'],
},
Expand Down
28 changes: 28 additions & 0 deletions packages/website/data/sponsors.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,27 @@
"totalDonations": 70000,
"website": "https://www.codiga.io"
},
{
"id": "frontendmasters",
"image": "https://avatars.githubusercontent.com/u/5613852?v=4",
"name": "Frontend Masters",
"totalDonations": 63327,
"website": "https://FrontendMasters.com"
},
{
"id": "DeepSource",
"image": "https://images.opencollective.com/deepsource/0f18cea/logo.png",
"name": "DeepSource",
"totalDonations": 60000,
"website": "https://deepsource.io/"
},
{
"id": "syntaxfm",
"image": "https://avatars.githubusercontent.com/u/130389858?v=4",
"name": "Syntax",
"totalDonations": 54449,
"website": "https://syntax.fm"
},
{
"id": "Future Processing",
"image": "https://images.opencollective.com/future-processing/1410d26/logo.png",
Expand Down Expand Up @@ -293,6 +307,13 @@
"totalDonations": 17000,
"website": "https://balsa.com/"
},
{
"id": "codecov",
"image": "https://avatars.githubusercontent.com/u/8226205?v=4",
"name": "Codecov",
"totalDonations": 15292,
"website": "https://codecov.io/"
},
{
"id": "THE PADDING",
"image": "https://images.opencollective.com/thepadding/55e79ad/logo.png",
Expand All @@ -314,6 +335,13 @@
"totalDonations": 14500,
"website": "https://now4real.com/"
},
{
"id": "getsentry",
"image": "https://avatars.githubusercontent.com/u/1396951?v=4",
"name": "Sentry",
"totalDonations": 14436,
"website": "https://sentry.io"
},
{
"id": "Knowledge Work",
"image": "https://images.opencollective.com/knowledge-work/f91b72d/logo.png",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function Sponsor({
return (
<Link
className={styles.sponsorLink}
href={sponsor.website ?? undefined}
href={sponsor.website}
title={sponsor.name}
rel="noopener sponsored"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
export interface SponsorData {
description?: string;
id: string;
image: string;
name: string;
tier?: string;
totalDonations: number;
website?: string;
website: string;
}
201 changes: 129 additions & 72 deletions tools/scripts/generate-sponsors.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,98 +2,155 @@ import * as fs from 'node:fs';
import * as path from 'node:path';

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

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

type MemberNodes = {
account: {
id: string;
imageUrl: string;
name: string;
website: string;
};
totalDonations: { valueInCents: number };
}[];

const excludedNames = new Set([
'Josh Goldberg', // Team member 💖
]);

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

const { members } = (
(await (
await fetch('https://api.opencollective.com/graphql/v2', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
{
collective(slug: "typescript-eslint") {
members(limit: 1000, role: BACKER) {
nodes {
account {
id
imageUrl
name
website
}
tier {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this attribute was unused in the response

amount {
valueInCents
}
orders(limit: 100) {
nodes {
amount {
valueInCents
}
}
}
}
totalDonations {
valueInCents
}
updatedAt
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this attribute was unused in the response

}
const jsonApiFetch = async <T,>(
api: string,
options?: RequestInit,
): Promise<T> => {
const url = `https://api.${api}`;
const response = await fetch(url, options);
if (!response.ok) {
console.error({
url,
response: { status: response.status, body: await response.text() },
});
throw new Error('API call failed.');
}
return (await response.json()) as T;
};

const openCollectiveSponsorsPromise = jsonApiFetch<{
data: {
collective: {
members: {
nodes: {
account: {
id: string;
imageUrl: string;
name: string;
website: string | null;
};
totalDonations: { valueInCents: number };
}[];
};
};
};
}>('opencollective.com/graphql/v2', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
{
collective(slug: "typescript-eslint") {
members(limit: 1000, role: BACKER) {
nodes {
account {
id
imageUrl
name
website
}
totalDonations {
valueInCents
}
}
}
`,
}),
})
).json()) as { data: { collective: { members: { nodes: MemberNodes } } } }
).data.collective;
}
}
`,
}),
}).then(({ data }) => {
// TODO: remove polyfill in Node 22
const groupBy = <T,>(
arr: T[],
fn: (item: T) => string,
): Record<string, T[]> => {
const grouped: Record<string, T[]> = {};
for (const item of arr) {
(grouped[fn(item)] ??= []).push(item);
}
return grouped;
};
return Object.entries(
groupBy(
data.collective.members.nodes,
({ account }) => account.name || account.id,
),
).flatMap(([id, members]) => {
const [
{
account: { website, ...account },
},
] = members;
return website
? {
id,
image: account.imageUrl,
name: account.name,
totalDonations: members.reduce(
(sum, { totalDonations }) => sum + totalDonations.valueInCents,
0,
),
website,
}
: [];
});
});

const sponsors = Object.entries(
// TODO: use Object.groupBy in Node 22
members.nodes.reduce<Record<string, MemberNodes>>((membersById, member) => {
const { account } = member;
(membersById[account.name || account.id] ??= []).push(member);
return membersById;
}, {}),
const thanksDevSponsorsPromise = jsonApiFetch<
Record<'dependers' | 'donors', ['gh' | 'gl', string, number][]>
>('thanks.dev/v1/vip/dependee/gh/typescript-eslint').then(async ({ donors }) =>
(
await Promise.all(
donors
/* GitLab does not have an API to get a user's profile. At the time of writing, only 13% of donors
from thanks.dev came from GitLab rather than GitHub, and none of them met the contribution
threshold. */
.filter(([site]) => site === 'gh')
.map(async ([, id, totalDonations]) => {
const { name, ...github } = await jsonApiFetch<
Record<'avatar_url' | 'blog', string> & {
name: string | null;
}
>(`github.com/users/${id}`);
return name
? {
id,
image: github.avatar_url,
name,
totalDonations,
website: github.blog || `https://github.com/${id}`,
}
: [];
}),
)
).flat(),
);

const sponsors = (
await Promise.all<SponsorData[]>([
openCollectiveSponsorsPromise,
thanksDevSponsorsPromise,
])
)
.map(([id, members]) => {
const [{ account }] = members;
return {
id,
image: account.imageUrl,
name: account.name,
totalDonations: members.reduce(
(sum, { totalDonations }) => sum + totalDonations.valueInCents,
0,
),
website: account.website,
};
})
.flat()
.filter(
({ id, name, totalDonations, website }) =>
({ id, name, totalDonations }) =>
!(
filteredTerms.some(filteredTerm =>
name.toLowerCase().includes(filteredTerm),
) ||
excludedNames.has(id) ||
totalDonations < 10000 ||
!website
totalDonations < 10000
),
)
.sort((a, b) => b.totalDonations - a.totalDonations);
Expand Down
Loading
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