From 4c3b9efe70a0f3b65a2bc982c7a0152fb4452381 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Fri, 18 Oct 2024 06:42:26 -0500 Subject: [PATCH 1/6] clean up --- tools/scripts/generate-sponsors.mts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/tools/scripts/generate-sponsors.mts b/tools/scripts/generate-sponsors.mts index e644261f6d61..130c390cd8c0 100644 --- a/tools/scripts/generate-sponsors.mts +++ b/tools/scripts/generate-sponsors.mts @@ -10,7 +10,7 @@ type MemberNodes = { id: string; imageUrl: string; name: string; - website: string; + website: string | null; }; totalDonations: { valueInCents: number }; }[]; @@ -38,22 +38,9 @@ const { members } = ( name website } - tier { - amount { - valueInCents - } - orders(limit: 100) { - nodes { - amount { - valueInCents - } - } - } - } totalDonations { valueInCents } - updatedAt } } } From 0c07457e7441c2696f87abf3d5bd89c12a43bc24 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Fri, 18 Oct 2024 11:29:01 -0500 Subject: [PATCH 2/6] finish --- packages/website/data/sponsors.json | 28 +++ .../components/FinancialContributors/types.ts | 4 +- tools/scripts/generate-sponsors.mts | 214 ++++++++++++------ 3 files changed, 170 insertions(+), 76 deletions(-) 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/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 130c390cd8c0..bcd8351e0e28 100644 --- a/tools/scripts/generate-sponsors.mts +++ b/tools/scripts/generate-sponsors.mts @@ -2,90 +2,158 @@ 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 | null; - }; - 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 - } - totalDonations { - valueInCents +const jsonApiFetch = async ( + api: string, + options?: RequestInit, +): Promise => { + const url = `https://api.${api}`; + const response = await fetch(url, options); + const body: unknown = await response.json(); + if (!response.ok) { + console.error({ url, response: { status: response.status, body } }); + throw new Error('API call failed.'); + } + return body as T; +}; + +fs.writeFileSync( + path.join(PACKAGES_WEBSITE, 'data', 'sponsors.json'), + `${JSON.stringify( + ( + await Promise.all([ + 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 + } + } } } } + `, + }), + }).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); } - } - `, - }), - }) - ).json()) as { data: { collective: { members: { nodes: MemberNodes } } } } -).data.collective; - -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; - }, {}), -) - .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, - }; - }) - .filter( - ({ id, name, totalDonations, website }) => - !( - filteredTerms.some(filteredTerm => - name.toLowerCase().includes(filteredTerm), - ) || - excludedNames.has(id) || - totalDonations < 10000 || - !website - ), - ) - .sort((a, b) => b.totalDonations - a.totalDonations); - -fs.writeFileSync( - path.join(PACKAGES_WEBSITE, 'data', 'sponsors.json'), - `${JSON.stringify(sponsors, null, 2)}\n`, + 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, + } + : []; + }); + }), + 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(), + ), + ]) + ) + .flat() + .filter( + ({ id, name, totalDonations }) => + !( + filteredTerms.some(filteredTerm => + name.toLowerCase().includes(filteredTerm), + ) || + excludedNames.has(id) || + totalDonations < 10000 + ), + ) + .sort((a, b) => b.totalDonations - a.totalDonations), + null, + 2, + )}\n`, ); From 708610de9f7d8cb2f03dd64f012bdf2fb4e79116 Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Fri, 18 Oct 2024 13:18:14 -0500 Subject: [PATCH 3/6] extract extra variables --- tools/scripts/generate-sponsors.mts | 257 ++++++++++++++-------------- 1 file changed, 129 insertions(+), 128 deletions(-) diff --git a/tools/scripts/generate-sponsors.mts b/tools/scripts/generate-sponsors.mts index bcd8351e0e28..69bc1543f5f0 100644 --- a/tools/scripts/generate-sponsors.mts +++ b/tools/scripts/generate-sponsors.mts @@ -18,142 +18,143 @@ const jsonApiFetch = async ( ): Promise => { const url = `https://api.${api}`; const response = await fetch(url, options); - const body: unknown = await response.json(); if (!response.ok) { - console.error({ url, response: { status: response.status, body } }); + console.error({ + url, + response: { status: response.status, body: await response.text() }, + }); throw new Error('API call failed.'); } - return body as T; + return (await response.json()) as T; }; -fs.writeFileSync( - path.join(PACKAGES_WEBSITE, 'data', 'sponsors.json'), - `${JSON.stringify( - ( - await Promise.all([ - jsonApiFetch<{ - data: { - collective: { - members: { - nodes: { - account: { - id: string; - imageUrl: string; - name: string; - website: string | null; - }; - totalDonations: { valueInCents: number }; - }[]; - }; - }; +const openCollectiveSponsorsPromise = jsonApiFetch<{ + data: { + collective: { + members: { + nodes: { + account: { + id: string; + imageUrl: string; + name: string; + website: string | null; }; - }>('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 - } - } - } - } + 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 } - `, - }), - }).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, - } - : []; - }); + } + } + } + `, + }), +}).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 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}`, + } + : []; }), - 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(), - ), - ]) ) - .flat() - .filter( - ({ id, name, totalDonations }) => - !( - filteredTerms.some(filteredTerm => - name.toLowerCase().includes(filteredTerm), - ) || - excludedNames.has(id) || - totalDonations < 10000 - ), - ) - .sort((a, b) => b.totalDonations - a.totalDonations), - null, - 2, - )}\n`, + ).flat(), +); + +const sponsors = ( + await Promise.all([ + openCollectiveSponsorsPromise, + thanksDevSponsorsPromise, + ]) +) + .flat() + .filter( + ({ id, name, totalDonations }) => + !( + filteredTerms.some(filteredTerm => + name.toLowerCase().includes(filteredTerm), + ) || + excludedNames.has(id) || + totalDonations < 10000 + ), + ) + .sort((a, b) => b.totalDonations - a.totalDonations); +fs.writeFileSync( + path.join(PACKAGES_WEBSITE, 'data', 'sponsors.json'), + `${JSON.stringify(sponsors, null, 2)}\n`, ); From d445ff79268b1613bfbd8c216fc07ffc89dcd99a Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Thu, 31 Oct 2024 16:55:03 -0500 Subject: [PATCH 4/6] add knip ignore --- knip.ts | 2 ++ 1 file changed, 2 insertions(+) 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'], }, From acff8d613adb68d9ba495785e3e4a1066b92db7a Mon Sep 17 00:00:00 2001 From: Abraham Guo Date: Thu, 31 Oct 2024 21:53:13 -0500 Subject: [PATCH 5/6] remove unnecessary nullish coalesce --- .../website/src/components/FinancialContributors/Sponsor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ( From 5e8c27cd962cd693f4af47ddc6e94d908877e313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Mon, 4 Nov 2024 08:56:18 -0500 Subject: [PATCH 6/6] Update tools/scripts/generate-sponsors.mts --- tools/scripts/generate-sponsors.mts | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/scripts/generate-sponsors.mts b/tools/scripts/generate-sponsors.mts index 69bc1543f5f0..4396c00cc47e 100644 --- a/tools/scripts/generate-sponsors.mts +++ b/tools/scripts/generate-sponsors.mts @@ -154,6 +154,7 @@ const sponsors = ( ), ) .sort((a, b) => b.totalDonations - a.totalDonations); + fs.writeFileSync( path.join(PACKAGES_WEBSITE, 'data', 'sponsors.json'), `${JSON.stringify(sponsors, null, 2)}\n`, 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