Skip to content
Draft
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: 1 addition & 1 deletion apps/site/components/Common/Supporters/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Supporter } from '#site/types';
import type { FC } from 'react';

type SupportersListProps = {
supporters: Array<Supporter<'opencollective'>>;
supporters: Array<Supporter<'opencollective' | 'github'>>;
};

const SupportersList: FC<SupportersListProps> = ({ supporters }) => (
Expand Down
160 changes: 158 additions & 2 deletions apps/site/next-data/generators/supportersData.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { OPENCOLLECTIVE_MEMBERS_URL } from '#site/next.constants.mjs';
import {
OPENCOLLECTIVE_MEMBERS_URL,
GITHUB_GRAPHQL_URL,
GITHUB_API_KEY,
} from '#site/next.constants.mjs';
import { fetchWithRetry } from '#site/util/fetch';

/**
Expand Down Expand Up @@ -26,4 +30,156 @@ async function fetchOpenCollectiveData() {
return members;
}

export default fetchOpenCollectiveData;
/**
* Fetches supporters data from Github API, filters active backers,
* and maps it to the Supporters type.
*
* @returns {Promise<Array<import('#site/types/supporters').GithubSponsorSupporter>>} Array of supporters
*/
async function fetchGithubSponsorsData() {
if (!GITHUB_API_KEY) {
return [];
}

const sponsors = [];

// Fetch sponsorship pages
let cursor = null;

while (true) {
const query = sponsorshipsQuery(cursor);
const data = await graphql(query);

if (data.errors) {
throw new Error(JSON.stringify(data.errors));
}

const nodeRes = data.data.user?.sponsorshipsAsMaintainer;
if (!nodeRes) {
break;
}

const { nodes, pageInfo } = nodeRes;
const mapped = nodes.map(n => {
const s = n.sponsor || n.sponsorEntity || n.sponsorEntity; // support different field names
return {
id: s?.id || null,
login: s?.login || null,
name: s?.name || s?.login || null,
avatar: s?.avatarUrl || null,
url: s?.websiteUrl || s?.url || null,
};
});

sponsors.push(...mapped);

if (!pageInfo.hasNextPage) {
break;
}

cursor = pageInfo.endCursor;
}
return sponsors;
}

function sponsorshipsQuery(cursor = null) {
return `
query {
organization(login: "nodejs") {
sponsorshipsAsMaintainer (first: 100, includePrivate: false, after: "${cursor}") {
nodes {
sponsor: sponsorEntity {
...on User {
id: databaseId,
name,
login,
avatarUrl,
url,
websiteUrl
}
...on Organization {
id: databaseId,
name,
login,
avatarUrl,
url,
websiteUrl
}
},
}
pageInfo {
endCursor
startCursor
hasNextPage
hasPreviousPage
}
}
}
}`;
}

// function donationsQuery(cursor = null) {
// return `
// query {
// organization(login: "nodejs") {
// sponsorsActivities (first: 100, includePrivate: false, after: "${cursor}") {
// nodes {
// id
// sponsor {
// ...on User {
// id: databaseId,
// name,
// login,
// avatarUrl,
// url,
// websiteUrl
// }
// ...on Organization {
// id: databaseId,
// name,
// login,
// avatarUrl,
// url,
// websiteUrl
// }
// },
// timestamp
// }
// }
// }
// }`;
// }

const graphql = async (query, variables = {}) => {
const res = await fetch(GITHUB_GRAPHQL_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${GITHUB_API_KEY}`,
Copy link
Member Author

Choose a reason for hiding this comment

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

@nodejs/web-infra Can I use this token, or is it better to create a new one?

Copy link
Member

@MattIPv4 MattIPv4 Jan 9, 2026

Choose a reason for hiding this comment

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

I don't believe NEXT_GITHUB_API_KEY exists. It was removed in #8163

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, okay, so can we add them then? It doesn’t require special permissions

Copy link
Member

Choose a reason for hiding this comment

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

fyi @ovflowd this should be for @openjs-vercel

Copy link
Member

Choose a reason for hiding this comment

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

I think we should make this clearer what it is, perhaps we call it NEXT_GITHUB_READ_API_KEY?

Copy link
Member

Choose a reason for hiding this comment

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

Yet? O.o

Copy link
Member

Choose a reason for hiding this comment

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

Issue on /admin?

Copy link
Member

@avivkeller avivkeller Jan 11, 2026

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

I bumped there. I can also do whatever you need. What do you need?

Copy link
Member Author

Choose a reason for hiding this comment

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

I bumped there. I can also do whatever you need. What do you need?

A token with the minimum required permissions, scoped to the Node.js organization, so it can be used to retrieve the sponsors.

},
body: JSON.stringify({ query, variables }),
});

if (!res.ok) {
const text = await res.text();
throw new Error(`GitHub API error: ${res.status} ${text}`);
}

return res.json();
};

/**
* Fetches supporters data from Open Collective API and GitHub Sponsors, filters active backers,
* and maps it to the Supporters type.
*
* @returns {Promise<Array<import('#site/types/supporters').OpenCollectiveSupporter | import('#site/types/supporters').GithubSponsorSupporter>>} Array of supporters
*/
async function sponsorsData() {
const sponsors = await Promise.all([
fetchGithubSponsorsData(),
fetchOpenCollectiveData(),
]);
return sponsors.flat();
}

export default sponsorsData;
5 changes: 5 additions & 0 deletions apps/site/next.constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,8 @@ export const VULNERABILITIES_URL =
*/
export const OPENCOLLECTIVE_MEMBERS_URL =
'https://opencollective.com/nodejs/members/all.json';

/**
* The location of Github Graphql API
*/
export const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql';
1 change: 1 addition & 0 deletions apps/site/types/supporters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export type Supporter<T extends string> = {
};

export type OpenCollectiveSupporter = Supporter<'opencollective'>;
export type GithubSponsorSupporter = Supporter<'github'>;
Loading