diff --git a/knip.ts b/knip.ts index d08e16ea701e..e3f8de90e898 100644 --- a/knip.ts +++ b/knip.ts @@ -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'], }, diff --git a/packages/website/data/sponsors.json b/packages/website/data/sponsors.json index 41459317465d..550d843804f3 100644 --- a/packages/website/data/sponsors.json +++ b/packages/website/data/sponsors.json @@ -132,6 +132,13 @@ "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", @@ -139,6 +146,13 @@ "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", @@ -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", @@ -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", diff --git a/packages/website/src/components/FinancialContributors/Sponsor.tsx b/packages/website/src/components/FinancialContributors/Sponsor.tsx index 7b4314ac7df0..b2ce3019ab6c 100644 --- a/packages/website/src/components/FinancialContributors/Sponsor.tsx +++ b/packages/website/src/components/FinancialContributors/Sponsor.tsx @@ -27,7 +27,7 @@ export function Sponsor({ return ( diff --git a/packages/website/src/components/FinancialContributors/types.ts b/packages/website/src/components/FinancialContributors/types.ts index 7bfc3911f398..24ac7a02ad75 100644 --- a/packages/website/src/components/FinancialContributors/types.ts +++ b/packages/website/src/components/FinancialContributors/types.ts @@ -1,9 +1,7 @@ export interface SponsorData { - description?: string; id: string; image: string; name: string; - tier?: string; totalDonations: number; - website?: string; + website: string; } diff --git a/tools/scripts/generate-sponsors.mts b/tools/scripts/generate-sponsors.mts index e644261f6d61..4396c00cc47e 100644 --- a/tools/scripts/generate-sponsors.mts +++ b/tools/scripts/generate-sponsors.mts @@ -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 { - amount { - valueInCents - } - orders(limit: 100) { - nodes { - amount { - valueInCents - } - } - } - } - totalDonations { - valueInCents - } - updatedAt - } +const jsonApiFetch = async ( + api: string, + options?: RequestInit, +): Promise => { + 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 = ( + arr: T[], + fn: (item: T) => string, + ): Record => { + const grouped: Record = {}; + 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>((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([ + 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); 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