mirror of
https://github.com/tauri-apps/tauri-docs.git
synced 2026-01-31 00:35:16 +01:00
refactor - use package
This commit is contained in:
10
packages/fetch-sponsors/config.ts
Normal file
10
packages/fetch-sponsors/config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const OPEN_COLLECTIVE_FILE = '../../src/data/openCollectiveData.json';
|
||||
export const GITHUB_SPONSORS_FILE = '../../src/data/githubSponsorsData.json';
|
||||
export const GITHUB_CONTRIBUTORS_FILE = '../../src/data/githubContributorsData.json';
|
||||
|
||||
export const PLATINUM_THRESHOLD = 5_000;
|
||||
export const GOLD_THRESHOLD = 500;
|
||||
export const SILVER_THRESHOLD = 100;
|
||||
|
||||
export const GH_IMAGE_DIMENSION = 64;
|
||||
export const OC_IMAGE_DIMENSION = 256;
|
||||
@@ -1,266 +1,266 @@
|
||||
import { Octokit } from '@octokit/core';
|
||||
import { paginateGraphQL, type PageInfoForward } from '@octokit/plugin-paginate-graphql';
|
||||
import { paginateRest } from '@octokit/plugin-paginate-rest';
|
||||
import { retry } from '@octokit/plugin-retry';
|
||||
import type { Endpoints } from '@octokit/types';
|
||||
import { throttling } from '@octokit/plugin-throttling';
|
||||
import path from 'node:path';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
// todo: write once to use on dev mode / prs
|
||||
// then re fetch on prod
|
||||
|
||||
// todo: move to package
|
||||
|
||||
export const DATA_FILE = path.resolve('./src/components/sponsors/_githubContributorsData.json');
|
||||
|
||||
export interface Contributor {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
total_contributions: number;
|
||||
}
|
||||
|
||||
type APIData<T extends keyof Endpoints> = Endpoints[T]['response']['data'];
|
||||
type Repo = APIData<'GET /orgs/{org}/repos'>[number];
|
||||
interface Review {
|
||||
login: string | undefined;
|
||||
avatarUrl: string | undefined;
|
||||
prNumber: number;
|
||||
labels: string[];
|
||||
}
|
||||
interface AugmentedRepo extends Repo {
|
||||
reviewComments: any[];
|
||||
issues: any[];
|
||||
reviews: Review[];
|
||||
}
|
||||
|
||||
const OctokitWithPlugins = Octokit.plugin(paginateRest, paginateGraphQL, retry, throttling);
|
||||
|
||||
export class StatsCollector {
|
||||
#org: string;
|
||||
#app: InstanceType<typeof OctokitWithPlugins>;
|
||||
#contributionThreshold: number;
|
||||
|
||||
constructor(opts: { org: string; token: string | undefined; contributionThreshold: number }) {
|
||||
this.#org = opts.org;
|
||||
if (!opts.token) {
|
||||
throw new Error('GITHUB_TOKEN is required');
|
||||
}
|
||||
this.#app = new OctokitWithPlugins({
|
||||
auth: opts.token,
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter, options, octokit, retryCount) => {
|
||||
octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
|
||||
|
||||
if (retryCount < 1) {
|
||||
// only retries once
|
||||
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
onSecondaryRateLimit: (retryAfter, options, octokit) => {
|
||||
// does not retry, only logs a warning
|
||||
octokit.log.warn(
|
||||
`SecondaryRateLimit detected for request ${options.method} ${options.url}`
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
this.#contributionThreshold = opts.contributionThreshold;
|
||||
}
|
||||
|
||||
async run() {
|
||||
const repos = await this.#getReposWithExtraStats();
|
||||
|
||||
const contributors: Record<string, Contributor> = {};
|
||||
|
||||
for (const repo of repos) {
|
||||
for (const issue of repo.issues) {
|
||||
const { user, pull_request } = issue;
|
||||
if (!user) {
|
||||
continue;
|
||||
}
|
||||
const { avatar_url, login } = user;
|
||||
const contributor = (contributors[login] =
|
||||
contributors[login] || this.#newContributor({ avatar_url, login }));
|
||||
if (pull_request) {
|
||||
contributor.total_contributions++;
|
||||
if (pull_request.merged_at) {
|
||||
contributor.total_contributions++;
|
||||
}
|
||||
} else {
|
||||
// is issue
|
||||
contributor.total_contributions++;
|
||||
}
|
||||
}
|
||||
|
||||
/** Temporary store for deduplicating multiple reviews on the same PR. */
|
||||
const reviewedPRs: Record<string, Set<number>> = {};
|
||||
|
||||
for (const review of repo.reviewComments) {
|
||||
const { user, pull_request_url } = review;
|
||||
const prNumber = parseInt(pull_request_url.split('/').pop()!);
|
||||
if (!user) {
|
||||
continue;
|
||||
}
|
||||
const { avatar_url, login } = user;
|
||||
const contributor = (contributors[login] =
|
||||
contributors[login] || this.#newContributor({ avatar_url, login }));
|
||||
const contributorReviews = (reviewedPRs[login] = reviewedPRs[login] || new Set());
|
||||
if (!contributorReviews.has(prNumber)) {
|
||||
contributor.total_contributions++;
|
||||
contributorReviews.add(prNumber);
|
||||
}
|
||||
}
|
||||
|
||||
for (const review of repo.reviews) {
|
||||
const { login, avatarUrl, prNumber } = review;
|
||||
if (!login || !avatarUrl) {
|
||||
continue;
|
||||
}
|
||||
const contributor = (contributors[login] =
|
||||
contributors[login] || this.#newContributor({ avatar_url: avatarUrl, login }));
|
||||
const contributorReviews = (reviewedPRs[login] = reviewedPRs[login] || new Set());
|
||||
if (!contributorReviews.has(prNumber)) {
|
||||
contributor.total_contributions++;
|
||||
contributorReviews.add(prNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter contributors based on threshold
|
||||
const topContributors = Object.values(contributors)
|
||||
.filter((contributor) => contributor.total_contributions >= this.#contributionThreshold)
|
||||
.sort((a, b) => b.total_contributions - a.total_contributions);
|
||||
|
||||
console.log(
|
||||
`${topContributors.length} contributors above threshold of ${this.#contributionThreshold} contributions`
|
||||
);
|
||||
console.log('saving');
|
||||
this.#writeData(topContributors);
|
||||
}
|
||||
|
||||
#newContributor({ avatar_url, login }: { avatar_url: string; login: string }): Contributor {
|
||||
return {
|
||||
login,
|
||||
avatar_url,
|
||||
total_contributions: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async #getRepos() {
|
||||
return (
|
||||
await this.#app.request(`GET /orgs/{org}/repos`, {
|
||||
org: this.#org,
|
||||
type: 'sources',
|
||||
})
|
||||
).data.filter((repo) => !repo.private);
|
||||
}
|
||||
|
||||
async #getAllIssuesAndPRs(repo: string) {
|
||||
console.log(`fetching issues and PRs for ${this.#org}/${repo}`);
|
||||
const issues = await this.#app.paginate('GET /repos/{owner}/{repo}/issues', {
|
||||
owner: this.#org,
|
||||
repo,
|
||||
per_page: 100,
|
||||
state: 'all',
|
||||
});
|
||||
console.log(`found ${issues.length} issues and PRs for ${this.#org}/${repo}`);
|
||||
return issues;
|
||||
}
|
||||
|
||||
async #getAllReviewComments(repo: string) {
|
||||
console.log(`fetching PR review comments for ${this.#org}/${repo}`);
|
||||
const reviews = await this.#app.paginate('GET /repos/{owner}/{repo}/pulls/comments', {
|
||||
owner: this.#org,
|
||||
repo,
|
||||
per_page: 100,
|
||||
});
|
||||
console.log(`found ${reviews.length} PR review comments for ${this.#org}/${repo}`);
|
||||
return reviews;
|
||||
}
|
||||
|
||||
async #getAllReviews(repo: string) {
|
||||
console.log(`fetching PR reviews for ${this.#org}/${repo}`);
|
||||
const {
|
||||
repository: {
|
||||
pullRequests: { nodes: pullRequests },
|
||||
},
|
||||
} = await this.#app.graphql.paginate<{
|
||||
repository: {
|
||||
pullRequests: {
|
||||
pageInfo: PageInfoForward;
|
||||
nodes: Array<{
|
||||
number: number;
|
||||
labels: { nodes: Array<{ name: string }> };
|
||||
latestReviews: {
|
||||
nodes: Array<{ author: null | { login: string; avatarUrl: string } }>;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}>(
|
||||
`
|
||||
query ($org: String!, $repo: String!, $cursor: String) {
|
||||
repository(owner: $org, name: $repo) {
|
||||
pullRequests(first: 100, after: $cursor) {
|
||||
nodes {
|
||||
number
|
||||
labels(first: 10) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
latestReviews(first: 15) {
|
||||
nodes {
|
||||
author {
|
||||
login
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ org: this.#org, repo }
|
||||
);
|
||||
const reviews: Review[] = [];
|
||||
for (const { number, labels, latestReviews } of pullRequests) {
|
||||
for (const { author } of latestReviews.nodes) {
|
||||
reviews.push({
|
||||
prNumber: number,
|
||||
labels: labels.nodes.map(({ name }) => name),
|
||||
login: author?.login,
|
||||
avatarUrl: author?.avatarUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(`found ${reviews.length} PR reviews for ${this.#org}/${repo}`);
|
||||
return reviews;
|
||||
}
|
||||
|
||||
async #getReposWithExtraStats() {
|
||||
const repos = await this.#getRepos();
|
||||
console.log(`found ${repos.length} repos`);
|
||||
const reposWithStats: AugmentedRepo[] = [];
|
||||
for (const repo of repos.slice(0, 2)) {
|
||||
reposWithStats.push({
|
||||
...repo,
|
||||
issues: await this.#getAllIssuesAndPRs(repo.name),
|
||||
reviewComments: await this.#getAllReviewComments(repo.name),
|
||||
reviews: await this.#getAllReviews(repo.name),
|
||||
});
|
||||
}
|
||||
return reposWithStats;
|
||||
}
|
||||
|
||||
#writeData(data: Contributor[]) {
|
||||
return writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
|
||||
}
|
||||
}
|
||||
import { Octokit } from '@octokit/core';
|
||||
import { paginateGraphQL, type PageInfoForward } from '@octokit/plugin-paginate-graphql';
|
||||
import { paginateRest } from '@octokit/plugin-paginate-rest';
|
||||
import { retry } from '@octokit/plugin-retry';
|
||||
import type { Endpoints } from '@octokit/types';
|
||||
import { throttling } from '@octokit/plugin-throttling';
|
||||
import path from 'node:path';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
// todo: write once to use on dev mode / prs
|
||||
// then re fetch on prod
|
||||
|
||||
// todo: move to package
|
||||
|
||||
export const DATA_FILE = path.resolve('./src/components/sponsors/_githubContributorsData.json');
|
||||
|
||||
export interface Contributor {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
total_contributions: number;
|
||||
}
|
||||
|
||||
type APIData<T extends keyof Endpoints> = Endpoints[T]['response']['data'];
|
||||
type Repo = APIData<'GET /orgs/{org}/repos'>[number];
|
||||
interface Review {
|
||||
login: string | undefined;
|
||||
avatarUrl: string | undefined;
|
||||
prNumber: number;
|
||||
labels: string[];
|
||||
}
|
||||
interface AugmentedRepo extends Repo {
|
||||
reviewComments: any[];
|
||||
issues: any[];
|
||||
reviews: Review[];
|
||||
}
|
||||
|
||||
const OctokitWithPlugins = Octokit.plugin(paginateRest, paginateGraphQL, retry, throttling);
|
||||
|
||||
export class StatsCollector {
|
||||
#org: string;
|
||||
#app: InstanceType<typeof OctokitWithPlugins>;
|
||||
#contributionThreshold: number;
|
||||
|
||||
constructor(opts: { org: string; token: string | undefined; contributionThreshold: number }) {
|
||||
this.#org = opts.org;
|
||||
if (!opts.token) {
|
||||
throw new Error('GITHUB_TOKEN is required');
|
||||
}
|
||||
this.#app = new OctokitWithPlugins({
|
||||
auth: opts.token,
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter, options, octokit, retryCount) => {
|
||||
octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
|
||||
|
||||
if (retryCount < 1) {
|
||||
// only retries once
|
||||
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
onSecondaryRateLimit: (retryAfter, options, octokit) => {
|
||||
// does not retry, only logs a warning
|
||||
octokit.log.warn(
|
||||
`SecondaryRateLimit detected for request ${options.method} ${options.url}`
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
this.#contributionThreshold = opts.contributionThreshold;
|
||||
}
|
||||
|
||||
async run() {
|
||||
const repos = await this.#getReposWithExtraStats();
|
||||
|
||||
const contributors: Record<string, Contributor> = {};
|
||||
|
||||
for (const repo of repos) {
|
||||
for (const issue of repo.issues) {
|
||||
const { user, pull_request } = issue;
|
||||
if (!user) {
|
||||
continue;
|
||||
}
|
||||
const { avatar_url, login } = user;
|
||||
const contributor = (contributors[login] =
|
||||
contributors[login] || this.#newContributor({ avatar_url, login }));
|
||||
if (pull_request) {
|
||||
contributor.total_contributions++;
|
||||
if (pull_request.merged_at) {
|
||||
contributor.total_contributions++;
|
||||
}
|
||||
} else {
|
||||
// is issue
|
||||
contributor.total_contributions++;
|
||||
}
|
||||
}
|
||||
|
||||
/** Temporary store for deduplicating multiple reviews on the same PR. */
|
||||
const reviewedPRs: Record<string, Set<number>> = {};
|
||||
|
||||
for (const review of repo.reviewComments) {
|
||||
const { user, pull_request_url } = review;
|
||||
const prNumber = parseInt(pull_request_url.split('/').pop()!);
|
||||
if (!user) {
|
||||
continue;
|
||||
}
|
||||
const { avatar_url, login } = user;
|
||||
const contributor = (contributors[login] =
|
||||
contributors[login] || this.#newContributor({ avatar_url, login }));
|
||||
const contributorReviews = (reviewedPRs[login] = reviewedPRs[login] || new Set());
|
||||
if (!contributorReviews.has(prNumber)) {
|
||||
contributor.total_contributions++;
|
||||
contributorReviews.add(prNumber);
|
||||
}
|
||||
}
|
||||
|
||||
for (const review of repo.reviews) {
|
||||
const { login, avatarUrl, prNumber } = review;
|
||||
if (!login || !avatarUrl) {
|
||||
continue;
|
||||
}
|
||||
const contributor = (contributors[login] =
|
||||
contributors[login] || this.#newContributor({ avatar_url: avatarUrl, login }));
|
||||
const contributorReviews = (reviewedPRs[login] = reviewedPRs[login] || new Set());
|
||||
if (!contributorReviews.has(prNumber)) {
|
||||
contributor.total_contributions++;
|
||||
contributorReviews.add(prNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter contributors based on threshold
|
||||
const topContributors = Object.values(contributors)
|
||||
.filter((contributor) => contributor.total_contributions >= this.#contributionThreshold)
|
||||
.sort((a, b) => b.total_contributions - a.total_contributions);
|
||||
|
||||
console.log(
|
||||
`${topContributors.length} contributors above threshold of ${this.#contributionThreshold} contributions`
|
||||
);
|
||||
console.log('saving');
|
||||
this.#writeData(topContributors);
|
||||
}
|
||||
|
||||
#newContributor({ avatar_url, login }: { avatar_url: string; login: string }): Contributor {
|
||||
return {
|
||||
login,
|
||||
avatar_url,
|
||||
total_contributions: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async #getRepos() {
|
||||
return (
|
||||
await this.#app.request(`GET /orgs/{org}/repos`, {
|
||||
org: this.#org,
|
||||
type: 'sources',
|
||||
})
|
||||
).data.filter((repo) => !repo.private);
|
||||
}
|
||||
|
||||
async #getAllIssuesAndPRs(repo: string) {
|
||||
console.log(`fetching issues and PRs for ${this.#org}/${repo}`);
|
||||
const issues = await this.#app.paginate('GET /repos/{owner}/{repo}/issues', {
|
||||
owner: this.#org,
|
||||
repo,
|
||||
per_page: 100,
|
||||
state: 'all',
|
||||
});
|
||||
console.log(`found ${issues.length} issues and PRs for ${this.#org}/${repo}`);
|
||||
return issues;
|
||||
}
|
||||
|
||||
async #getAllReviewComments(repo: string) {
|
||||
console.log(`fetching PR review comments for ${this.#org}/${repo}`);
|
||||
const reviews = await this.#app.paginate('GET /repos/{owner}/{repo}/pulls/comments', {
|
||||
owner: this.#org,
|
||||
repo,
|
||||
per_page: 100,
|
||||
});
|
||||
console.log(`found ${reviews.length} PR review comments for ${this.#org}/${repo}`);
|
||||
return reviews;
|
||||
}
|
||||
|
||||
async #getAllReviews(repo: string) {
|
||||
console.log(`fetching PR reviews for ${this.#org}/${repo}`);
|
||||
const {
|
||||
repository: {
|
||||
pullRequests: { nodes: pullRequests },
|
||||
},
|
||||
} = await this.#app.graphql.paginate<{
|
||||
repository: {
|
||||
pullRequests: {
|
||||
pageInfo: PageInfoForward;
|
||||
nodes: Array<{
|
||||
number: number;
|
||||
labels: { nodes: Array<{ name: string }> };
|
||||
latestReviews: {
|
||||
nodes: Array<{ author: null | { login: string; avatarUrl: string } }>;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}>(
|
||||
`
|
||||
query ($org: String!, $repo: String!, $cursor: String) {
|
||||
repository(owner: $org, name: $repo) {
|
||||
pullRequests(first: 100, after: $cursor) {
|
||||
nodes {
|
||||
number
|
||||
labels(first: 10) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
latestReviews(first: 15) {
|
||||
nodes {
|
||||
author {
|
||||
login
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ org: this.#org, repo }
|
||||
);
|
||||
const reviews: Review[] = [];
|
||||
for (const { number, labels, latestReviews } of pullRequests) {
|
||||
for (const { author } of latestReviews.nodes) {
|
||||
reviews.push({
|
||||
prNumber: number,
|
||||
labels: labels.nodes.map(({ name }) => name),
|
||||
login: author?.login,
|
||||
avatarUrl: author?.avatarUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(`found ${reviews.length} PR reviews for ${this.#org}/${repo}`);
|
||||
return reviews;
|
||||
}
|
||||
|
||||
async #getReposWithExtraStats() {
|
||||
const repos = await this.#getRepos();
|
||||
console.log(`found ${repos.length} repos`);
|
||||
const reposWithStats: AugmentedRepo[] = [];
|
||||
for (const repo of repos.slice(0, 2)) {
|
||||
reposWithStats.push({
|
||||
...repo,
|
||||
issues: await this.#getAllIssuesAndPRs(repo.name),
|
||||
reviewComments: await this.#getAllReviewComments(repo.name),
|
||||
reviews: await this.#getAllReviews(repo.name),
|
||||
});
|
||||
}
|
||||
return reposWithStats;
|
||||
}
|
||||
|
||||
#writeData(data: Contributor[]) {
|
||||
return writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
|
||||
}
|
||||
}
|
||||
39
packages/fetch-sponsors/githubSponsors.ts
Normal file
39
packages/fetch-sponsors/githubSponsors.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { GH_IMAGE_DIMENSION } from './config';
|
||||
import type { GitHubSponsor } from './types';
|
||||
import { q } from './utils';
|
||||
|
||||
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
|
||||
|
||||
export async function fetchGitHubSponsors() {
|
||||
if (!GITHUB_TOKEN) {
|
||||
console.error('GITHUB_TOKEN not set');
|
||||
return [];
|
||||
}
|
||||
|
||||
// https://docs.github.com/graphql
|
||||
const query = `query {
|
||||
organization(login:"tauri-apps") {
|
||||
sponsors(first: 100) {
|
||||
nodes {
|
||||
... on Actor {
|
||||
login,
|
||||
avatarUrl(size: ${GH_IMAGE_DIMENSION})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const data = await q(query, 'https://api.opencollective.com/graphql/v2', 'Open Collective', {
|
||||
Authorization: `bearer ${GITHUB_TOKEN}`,
|
||||
});
|
||||
|
||||
return data.organization.sponsors.nodes
|
||||
.map(
|
||||
(node: any): GitHubSponsor => ({
|
||||
name: node.login,
|
||||
avatarUrl: node.avatarUrl,
|
||||
})
|
||||
)
|
||||
.sort((a: GitHubSponsor, b: GitHubSponsor) => a.name.localeCompare(b.name));
|
||||
}
|
||||
12
packages/fetch-sponsors/main.ts
Normal file
12
packages/fetch-sponsors/main.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { GITHUB_SPONSORS_FILE, OPEN_COLLECTIVE_FILE } from './config';
|
||||
import { fetchGitHubSponsors } from './githubSponsors';
|
||||
import { fetchOpenCollectiveData } from './openCollective';
|
||||
import { checkAndWriteData } from './utils';
|
||||
|
||||
async function main() {
|
||||
await checkAndWriteData(OPEN_COLLECTIVE_FILE, fetchOpenCollectiveData);
|
||||
await checkAndWriteData(GITHUB_SPONSORS_FILE, fetchGitHubSponsors);
|
||||
// todo: contributors
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
IMAGE_DIMENSION,
|
||||
type Sponsor,
|
||||
type Tier,
|
||||
} from '@components/sponsors/OpenCollective/_types';
|
||||
|
||||
export const PLATINUM_THRESHOLD = 5_000;
|
||||
export const GOLD_THRESHOLD = 500;
|
||||
export const SILVER_THRESHOLD = 100;
|
||||
import { GOLD_THRESHOLD, PLATINUM_THRESHOLD, SILVER_THRESHOLD, OC_IMAGE_DIMENSION } from './config';
|
||||
import { type OpenCollectiveSponsor, type Tier } from './types';
|
||||
import { q } from './utils';
|
||||
|
||||
export async function fetchOpenCollectiveData() {
|
||||
const filteredSlugs = ['github-sponsors'];
|
||||
@@ -19,7 +13,7 @@ export async function fetchOpenCollectiveData() {
|
||||
account {
|
||||
name
|
||||
type
|
||||
imageUrl(height: ${IMAGE_DIMENSION}, format: jpg)
|
||||
imageUrl(height: ${OC_IMAGE_DIMENSION}, format: jpg)
|
||||
slug
|
||||
isIncognito
|
||||
}
|
||||
@@ -31,25 +25,10 @@ export async function fetchOpenCollectiveData() {
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const res = await fetch('https://api.opencollective.com/graphql/v2', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw Error(
|
||||
`Open Collective query failed: ${res.status} ${res.statusText} \n ${JSON.stringify(await res.json(), null, 2)}, `
|
||||
);
|
||||
}
|
||||
const data = await q(query, 'https://api.opencollective.com/graphql/v2', 'Open Collective');
|
||||
|
||||
// TODO: handle currency
|
||||
|
||||
const openCollectiveData = (await res.json()).data;
|
||||
return openCollectiveData.collective.contributors.nodes
|
||||
return data.collective.contributors.nodes
|
||||
.filter(
|
||||
(node: any) =>
|
||||
!node.account.isIncognito &&
|
||||
@@ -58,7 +37,7 @@ export async function fetchOpenCollectiveData() {
|
||||
node.account.name != 'Guest'
|
||||
)
|
||||
.sort((a: any, b: any) => b.totalAmountContributed.value - a.totalAmountContributed.value)
|
||||
.map((node: any): Sponsor => {
|
||||
.map((node: any): OpenCollectiveSponsor => {
|
||||
let tier: Tier;
|
||||
let amount = node.totalAmountContributed.value;
|
||||
if (amount >= PLATINUM_THRESHOLD) {
|
||||
25
packages/fetch-sponsors/package.json
Normal file
25
packages/fetch-sponsors/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "fetch-sponsors",
|
||||
"version": "1.0.0",
|
||||
"private": "true",
|
||||
"type": "module",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"dependencies": {
|
||||
"@octokit/core": "^7.0.3",
|
||||
"@octokit/plugin-paginate-graphql": "^6.0.0",
|
||||
"@octokit/plugin-paginate-rest": "^13.1.1",
|
||||
"@octokit/plugin-retry": "^8.0.1",
|
||||
"@octokit/plugin-throttling": "^11.0.1",
|
||||
"@octokit/types": "^14.1.0",
|
||||
"@types/node": "^22.15.21",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
109
packages/fetch-sponsors/tsconfig.json
Normal file
109
packages/fetch-sponsors/tsconfig.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
export type Sponsor = {
|
||||
export type GitHubSponsor = {
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
profileUrl?: string;
|
||||
};
|
||||
|
||||
export type OpenCollectiveSponsor = {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
@@ -8,4 +14,3 @@ export type Sponsor = {
|
||||
};
|
||||
|
||||
export type Tier = 'platinum' | 'gold' | 'silver' | 'bronze';
|
||||
export const IMAGE_DIMENSION = 256;
|
||||
33
packages/fetch-sponsors/utils.ts
Normal file
33
packages/fetch-sponsors/utils.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
export async function q(query: string, url: string, name: string, headers?: any) {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw Error(
|
||||
`${name} query failed: ${res.status} ${res.statusText} \n ${JSON.stringify(await res.json(), null, 2)}, `
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await res.json()).data;
|
||||
return data;
|
||||
}
|
||||
|
||||
// TODO: override on prod
|
||||
export async function checkAndWriteData(filePath: string, fetcher: any) {
|
||||
let data = [];
|
||||
if (fs.existsSync(filePath)) {
|
||||
data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
} else {
|
||||
data = await fetcher();
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
164
pnpm-lock.yaml
generated
164
pnpm-lock.yaml
generated
@@ -120,6 +120,33 @@ importers:
|
||||
specifier: ^5.3.3
|
||||
version: 5.5.4
|
||||
|
||||
packages/fetch-sponsors:
|
||||
dependencies:
|
||||
'@octokit/core':
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3
|
||||
'@octokit/plugin-paginate-graphql':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(@octokit/core@7.0.3)
|
||||
'@octokit/plugin-paginate-rest':
|
||||
specifier: ^13.1.1
|
||||
version: 13.1.1(@octokit/core@7.0.3)
|
||||
'@octokit/plugin-retry':
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1(@octokit/core@7.0.3)
|
||||
'@octokit/plugin-throttling':
|
||||
specifier: ^11.0.1
|
||||
version: 11.0.1(@octokit/core@7.0.3)
|
||||
'@octokit/types':
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0
|
||||
'@types/node':
|
||||
specifier: ^22.15.21
|
||||
version: 22.15.21
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
|
||||
packages/js-api-generator:
|
||||
dependencies:
|
||||
github-slugger:
|
||||
@@ -1163,6 +1190,60 @@ packages:
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@octokit/auth-token@6.0.0':
|
||||
resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/core@7.0.3':
|
||||
resolution: {integrity: sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/endpoint@11.0.0':
|
||||
resolution: {integrity: sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/graphql@9.0.1':
|
||||
resolution: {integrity: sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/openapi-types@25.1.0':
|
||||
resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==}
|
||||
|
||||
'@octokit/plugin-paginate-graphql@6.0.0':
|
||||
resolution: {integrity: sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==}
|
||||
engines: {node: '>= 20'}
|
||||
peerDependencies:
|
||||
'@octokit/core': '>=6'
|
||||
|
||||
'@octokit/plugin-paginate-rest@13.1.1':
|
||||
resolution: {integrity: sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==}
|
||||
engines: {node: '>= 20'}
|
||||
peerDependencies:
|
||||
'@octokit/core': '>=6'
|
||||
|
||||
'@octokit/plugin-retry@8.0.1':
|
||||
resolution: {integrity: sha512-KUoYR77BjF5O3zcwDQHRRZsUvJwepobeqiSSdCJ8lWt27FZExzb0GgVxrhhfuyF6z2B2zpO0hN5pteni1sqWiw==}
|
||||
engines: {node: '>= 20'}
|
||||
peerDependencies:
|
||||
'@octokit/core': '>=7'
|
||||
|
||||
'@octokit/plugin-throttling@11.0.1':
|
||||
resolution: {integrity: sha512-S+EVhy52D/272L7up58dr3FNSMXWuNZolkL4zMJBNIfIxyZuUcczsQAU4b5w6dewJXnKYVgSHSV5wxitMSW1kw==}
|
||||
engines: {node: '>= 20'}
|
||||
peerDependencies:
|
||||
'@octokit/core': ^7.0.0
|
||||
|
||||
'@octokit/request-error@7.0.0':
|
||||
resolution: {integrity: sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/request@10.0.3':
|
||||
resolution: {integrity: sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/types@14.1.0':
|
||||
resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==}
|
||||
|
||||
'@oslojs/encoding@1.1.0':
|
||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||
|
||||
@@ -1754,12 +1835,18 @@ packages:
|
||||
bcp-47@2.1.0:
|
||||
resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==}
|
||||
|
||||
before-after-hook@4.0.0:
|
||||
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
|
||||
|
||||
blob-to-buffer@1.2.9:
|
||||
resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==}
|
||||
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
bottleneck@2.19.5:
|
||||
resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==}
|
||||
|
||||
boxen@8.0.1:
|
||||
resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2241,6 +2328,9 @@ packages:
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
fast-content-type-parse@3.0.0:
|
||||
resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -3732,6 +3822,9 @@ packages:
|
||||
unist-util-visit@5.0.0:
|
||||
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
|
||||
|
||||
universal-user-agent@7.0.3:
|
||||
resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==}
|
||||
|
||||
universalify@2.0.1:
|
||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -5234,6 +5327,69 @@ snapshots:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.17.1
|
||||
|
||||
'@octokit/auth-token@6.0.0': {}
|
||||
|
||||
'@octokit/core@7.0.3':
|
||||
dependencies:
|
||||
'@octokit/auth-token': 6.0.0
|
||||
'@octokit/graphql': 9.0.1
|
||||
'@octokit/request': 10.0.3
|
||||
'@octokit/request-error': 7.0.0
|
||||
'@octokit/types': 14.1.0
|
||||
before-after-hook: 4.0.0
|
||||
universal-user-agent: 7.0.3
|
||||
|
||||
'@octokit/endpoint@11.0.0':
|
||||
dependencies:
|
||||
'@octokit/types': 14.1.0
|
||||
universal-user-agent: 7.0.3
|
||||
|
||||
'@octokit/graphql@9.0.1':
|
||||
dependencies:
|
||||
'@octokit/request': 10.0.3
|
||||
'@octokit/types': 14.1.0
|
||||
universal-user-agent: 7.0.3
|
||||
|
||||
'@octokit/openapi-types@25.1.0': {}
|
||||
|
||||
'@octokit/plugin-paginate-graphql@6.0.0(@octokit/core@7.0.3)':
|
||||
dependencies:
|
||||
'@octokit/core': 7.0.3
|
||||
|
||||
'@octokit/plugin-paginate-rest@13.1.1(@octokit/core@7.0.3)':
|
||||
dependencies:
|
||||
'@octokit/core': 7.0.3
|
||||
'@octokit/types': 14.1.0
|
||||
|
||||
'@octokit/plugin-retry@8.0.1(@octokit/core@7.0.3)':
|
||||
dependencies:
|
||||
'@octokit/core': 7.0.3
|
||||
'@octokit/request-error': 7.0.0
|
||||
'@octokit/types': 14.1.0
|
||||
bottleneck: 2.19.5
|
||||
|
||||
'@octokit/plugin-throttling@11.0.1(@octokit/core@7.0.3)':
|
||||
dependencies:
|
||||
'@octokit/core': 7.0.3
|
||||
'@octokit/types': 14.1.0
|
||||
bottleneck: 2.19.5
|
||||
|
||||
'@octokit/request-error@7.0.0':
|
||||
dependencies:
|
||||
'@octokit/types': 14.1.0
|
||||
|
||||
'@octokit/request@10.0.3':
|
||||
dependencies:
|
||||
'@octokit/endpoint': 11.0.0
|
||||
'@octokit/request-error': 7.0.0
|
||||
'@octokit/types': 14.1.0
|
||||
fast-content-type-parse: 3.0.0
|
||||
universal-user-agent: 7.0.3
|
||||
|
||||
'@octokit/types@14.1.0':
|
||||
dependencies:
|
||||
'@octokit/openapi-types': 25.1.0
|
||||
|
||||
'@oslojs/encoding@1.1.0': {}
|
||||
|
||||
'@pagefind/darwin-arm64@1.3.0':
|
||||
@@ -5879,10 +6035,14 @@ snapshots:
|
||||
is-alphanumerical: 2.0.1
|
||||
is-decimal: 2.0.1
|
||||
|
||||
before-after-hook@4.0.0: {}
|
||||
|
||||
blob-to-buffer@1.2.9: {}
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
bottleneck@2.19.5: {}
|
||||
|
||||
boxen@8.0.1:
|
||||
dependencies:
|
||||
ansi-align: 3.0.1
|
||||
@@ -6376,6 +6536,8 @@ snapshots:
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fast-content-type-parse@3.0.0: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-glob@3.3.2:
|
||||
@@ -8492,6 +8654,8 @@ snapshots:
|
||||
unist-util-is: 6.0.0
|
||||
unist-util-visit-parents: 6.0.1
|
||||
|
||||
universal-user-agent@7.0.3: {}
|
||||
|
||||
universalify@2.0.1: {}
|
||||
|
||||
unstorage@1.16.1:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
// TODO
|
||||
import { readFileSync } from 'fs';
|
||||
import { DATA_FILE, StatsCollector } from '../../../utils/fetchGitHubContributorsData';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { DATA_FILE, StatsCollector } from 'packages/fetch-sponsors/fetchGitHubContributorsData';
|
||||
|
||||
// todo: existsSync check or do everything on the script
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
---
|
||||
// todo: share this with open collective or at least move to fetchGitHubSponsorsData.ts
|
||||
|
||||
import { Image } from 'astro:assets';
|
||||
|
||||
const GITHUB_TOKEN = import.meta.env.GITHUB_TOKEN;
|
||||
|
||||
type Sponsor = {
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
profileUrl?: string;
|
||||
};
|
||||
|
||||
const IMAGE_DIMENSION = 64;
|
||||
const gitHubSponsors = await getGitHubSponsors();
|
||||
const gitHubSponsorsLoaded = gitHubSponsors.length > 0;
|
||||
|
||||
async function getGitHubSponsors(): Promise<any[]> {
|
||||
if (!GITHUB_TOKEN) {
|
||||
console.error('GITHUB_TOKEN not set');
|
||||
return [];
|
||||
}
|
||||
|
||||
// https://docs.github.com/graphql
|
||||
const gitHubQuery = `query {
|
||||
organization(login:"tauri-apps") {
|
||||
sponsors(first: 100) {
|
||||
nodes {
|
||||
... on Actor {
|
||||
login,
|
||||
avatarUrl(size: ${IMAGE_DIMENSION})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const res = await fetch('https://api.github.com/graphql', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query: gitHubQuery }),
|
||||
headers: {
|
||||
Authorization: `bearer ${GITHUB_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw Error(
|
||||
`gh query failed: ${res.status} ${res.statusText} \n ${JSON.stringify(await res.json(), null, 2)}, `
|
||||
);
|
||||
}
|
||||
|
||||
const gitHubSponsorData = (await res.json()).data;
|
||||
return gitHubSponsorData.organization.sponsors.nodes
|
||||
.map(
|
||||
(node: any): Sponsor => ({
|
||||
name: node.login,
|
||||
avatarUrl: node.avatarUrl,
|
||||
})
|
||||
)
|
||||
.sort((a: Sponsor, b: Sponsor) => a.name.localeCompare(b.name));
|
||||
}
|
||||
---
|
||||
|
||||
{
|
||||
gitHubSponsorsLoaded && (
|
||||
<div class="container">
|
||||
{gitHubSponsors.map((sponsor) => (
|
||||
<div>
|
||||
{sponsor.avatarUrl && (
|
||||
<a
|
||||
href={`https://github.com/${sponsor.name}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image class="avatar" inferSize src={sponsor.avatarUrl} alt={sponsor.name} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<style>
|
||||
/* todo: extract shared css */
|
||||
:root {
|
||||
--overlap: 16px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.container a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-inline-start: var(--overlap);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
margin-inline-start: calc(var(--overlap) * -1);
|
||||
aspect-ratio: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 1s ease;
|
||||
will-change: transform;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar:hover {
|
||||
transform: scale(1.2);
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
74
src/components/sponsors/GitHub/Sponsors.astro
Normal file
74
src/components/sponsors/GitHub/Sponsors.astro
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { GITHUB_SPONSORS_FILE } from 'packages/fetch-sponsors/config';
|
||||
import type { GitHubSponsor } from 'packages/fetch-sponsors/types';
|
||||
|
||||
let gitHubSponsors = [];
|
||||
if (existsSync(GITHUB_SPONSORS_FILE)) {
|
||||
gitHubSponsors = JSON.parse(readFileSync(GITHUB_SPONSORS_FILE, 'utf-8'));
|
||||
} else {
|
||||
throw new Error('GitHub sponsors data file not found.');
|
||||
}
|
||||
const gitHubSponsorsLoaded = gitHubSponsors.length > 0;
|
||||
---
|
||||
|
||||
{
|
||||
gitHubSponsorsLoaded && (
|
||||
<div class="container">
|
||||
{gitHubSponsors.map((sponsor: GitHubSponsor) => (
|
||||
<div>
|
||||
{sponsor.avatarUrl && (
|
||||
<a
|
||||
href={`https://github.com/${sponsor.name}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image class="avatar" inferSize src={sponsor.avatarUrl} alt={sponsor.name} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<style>
|
||||
/* todo: extract shared css */
|
||||
:root {
|
||||
--overlap: 16px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.container a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-inline-start: var(--overlap);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
margin-inline-start: calc(var(--overlap) * -1);
|
||||
aspect-ratio: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 1s ease;
|
||||
will-change: transform;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar:hover {
|
||||
transform: scale(1.2);
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,20 @@
|
||||
---
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import Contributor from './Contributor.astro';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import {
|
||||
fetchOpenCollectiveData,
|
||||
GOLD_THRESHOLD,
|
||||
OPEN_COLLECTIVE_FILE,
|
||||
PLATINUM_THRESHOLD,
|
||||
GOLD_THRESHOLD,
|
||||
SILVER_THRESHOLD,
|
||||
} from '@utils/fetchOpenCollectiveData';
|
||||
import type { Sponsor } from './_types';
|
||||
} from 'packages/fetch-sponsors/config';
|
||||
import { type OpenCollectiveSponsor as Sponsor } from 'packages/fetch-sponsors/types';
|
||||
|
||||
const DATA_FILE = path.resolve('./src/components/sponsors/_openCollectiveData.json');
|
||||
import OcAvatar from './OcAvatar.astro';
|
||||
|
||||
let openCollectiveSponsors = [];
|
||||
if (fs.existsSync(DATA_FILE)) {
|
||||
openCollectiveSponsors = JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8'));
|
||||
if (existsSync(OPEN_COLLECTIVE_FILE)) {
|
||||
openCollectiveSponsors = JSON.parse(readFileSync(OPEN_COLLECTIVE_FILE, 'utf-8'));
|
||||
} else {
|
||||
openCollectiveSponsors = await fetchOpenCollectiveData();
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify(openCollectiveSponsors, null, 2));
|
||||
throw new Error('Open Collective data file not found.');
|
||||
}
|
||||
|
||||
const sponsorsByTier = {
|
||||
@@ -32,10 +29,10 @@ const goldSponsors = sponsorsByTier.gold;
|
||||
const silverSponsors = sponsorsByTier.silver;
|
||||
|
||||
// disabled
|
||||
const bronzeSponsors = sponsorsByTier.bronze
|
||||
.filter((sponsor: Sponsor) => sponsor.name !== 'Incognito')
|
||||
.slice(0, 50);
|
||||
const totalBronzeSponsors = sponsorsByTier.bronze.length;
|
||||
// const bronzeSponsors = sponsorsByTier.bronze
|
||||
// .filter((sponsor: Sponsor) => sponsor.name !== 'Incognito')
|
||||
// .slice(0, 50);
|
||||
// const totalBronzeSponsors = sponsorsByTier.bronze.length;
|
||||
---
|
||||
|
||||
{
|
||||
@@ -43,8 +40,8 @@ const totalBronzeSponsors = sponsorsByTier.bronze.length;
|
||||
<div class="tier-section">
|
||||
<h4>Platinum Sponsors (${PLATINUM_THRESHOLD.toLocaleString()}+)</h4>
|
||||
<div class="sponsor-container platinum">
|
||||
{platinumSponsors.map((sponsor) => (
|
||||
<Contributor {sponsor} />
|
||||
{platinumSponsors.map((sponsor: Sponsor) => (
|
||||
<OcAvatar {sponsor} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,8 +53,8 @@ const totalBronzeSponsors = sponsorsByTier.bronze.length;
|
||||
<div class="tier-section">
|
||||
<h4>Gold Sponsors (${GOLD_THRESHOLD.toLocaleString()}+)</h4>
|
||||
<div class="sponsor-container gold">
|
||||
{goldSponsors.map((sponsor) => (
|
||||
<Contributor {sponsor} />
|
||||
{goldSponsors.map((sponsor: Sponsor) => (
|
||||
<OcAvatar {sponsor} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,9 +66,9 @@ const totalBronzeSponsors = sponsorsByTier.bronze.length;
|
||||
<div class="tier-section">
|
||||
<h4>Silver Sponsors (${SILVER_THRESHOLD.toLocaleString()}+)</h4>
|
||||
<div class="sponsor-container silver">
|
||||
{silverSponsors.map((sponsor) => (
|
||||
{silverSponsors.map((sponsor: Sponsor) => (
|
||||
<div>
|
||||
<Contributor {sponsor} />
|
||||
<OcAvatar {sponsor} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -85,7 +82,7 @@ const totalBronzeSponsors = sponsorsByTier.bronze.length;
|
||||
<h4>Bronze Sponsors</h4>
|
||||
<div class="sponsor-list">
|
||||
{bronzeSponsors.map((sponsor, index) => (
|
||||
<Contributor {sponsor} needComma={index < bronzeSponsors.length - 1} />
|
||||
<OcAvatar {sponsor} needComma={index < bronzeSponsors.length - 1} />
|
||||
))}
|
||||
{totalBronzeSponsors > bronzeSponsors.length && (
|
||||
<span class="more-sponsors">
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import { IMAGE_DIMENSION, type Sponsor } from './_types';
|
||||
import { OC_IMAGE_DIMENSION } from 'packages/fetch-sponsors/config';
|
||||
import { type OpenCollectiveSponsor as Sponsor } from 'packages/fetch-sponsors/types';
|
||||
|
||||
interface Props {
|
||||
sponsor: Sponsor;
|
||||
|
||||
needComma?: boolean;
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ const roundingStyle: Record<Sponsor['type'], string> = {
|
||||
loading={'lazy'}
|
||||
src={sponsor.avatarUrl}
|
||||
alt={sponsor.name}
|
||||
width={IMAGE_DIMENSION}
|
||||
height={IMAGE_DIMENSION}
|
||||
width={OC_IMAGE_DIMENSION}
|
||||
height={OC_IMAGE_DIMENSION}
|
||||
class={`image ${sponsor.tier} ${roundingStyle[sponsor.type]}`}
|
||||
/>
|
||||
</a>
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import OpenCollective from './OpenCollective/Main.astro';
|
||||
import GitHubSponsor from './GitHub/Main.astro';
|
||||
import OpenCollective from './OpenCollective/Collective.astro';
|
||||
import GitHubSponsor from './GitHub/Sponsors.astro';
|
||||
import ServiceProvider from './ServiceProviders/Main.astro';
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user