diff --git a/packages/fetch-sponsors/config.ts b/packages/fetch-sponsors/config.ts new file mode 100644 index 000000000..f60036d75 --- /dev/null +++ b/packages/fetch-sponsors/config.ts @@ -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; diff --git a/src/utils/fetchGitHubContributorsData.ts b/packages/fetch-sponsors/fetchGitHubContributorsData.ts similarity index 96% rename from src/utils/fetchGitHubContributorsData.ts rename to packages/fetch-sponsors/fetchGitHubContributorsData.ts index 88140ba85..c49af8db7 100644 --- a/src/utils/fetchGitHubContributorsData.ts +++ b/packages/fetch-sponsors/fetchGitHubContributorsData.ts @@ -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 = 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; - #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 = {}; - - 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> = {}; - - 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 = 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; + #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 = {}; + + 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> = {}; + + 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'); + } +} diff --git a/packages/fetch-sponsors/githubSponsors.ts b/packages/fetch-sponsors/githubSponsors.ts new file mode 100644 index 000000000..d48f8ef7a --- /dev/null +++ b/packages/fetch-sponsors/githubSponsors.ts @@ -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)); +} diff --git a/packages/fetch-sponsors/main.ts b/packages/fetch-sponsors/main.ts new file mode 100644 index 000000000..f4067de3d --- /dev/null +++ b/packages/fetch-sponsors/main.ts @@ -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(); diff --git a/src/utils/fetchOpenCollectiveData.ts b/packages/fetch-sponsors/openCollective.ts similarity index 63% rename from src/utils/fetchOpenCollectiveData.ts rename to packages/fetch-sponsors/openCollective.ts index 42d51ead1..6a9942b67 100644 --- a/src/utils/fetchOpenCollectiveData.ts +++ b/packages/fetch-sponsors/openCollective.ts @@ -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) { diff --git a/packages/fetch-sponsors/package.json b/packages/fetch-sponsors/package.json new file mode 100644 index 000000000..7c2b27081 --- /dev/null +++ b/packages/fetch-sponsors/package.json @@ -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" + } +} diff --git a/packages/fetch-sponsors/tsconfig.json b/packages/fetch-sponsors/tsconfig.json new file mode 100644 index 000000000..bb2f7f662 --- /dev/null +++ b/packages/fetch-sponsors/tsconfig.json @@ -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 ''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. */ + } +} diff --git a/src/components/sponsors/OpenCollective/_types.ts b/packages/fetch-sponsors/types.ts similarity index 59% rename from src/components/sponsors/OpenCollective/_types.ts rename to packages/fetch-sponsors/types.ts index d2214a3b1..f065b309e 100644 --- a/src/components/sponsors/OpenCollective/_types.ts +++ b/packages/fetch-sponsors/types.ts @@ -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; diff --git a/packages/fetch-sponsors/utils.ts b/packages/fetch-sponsors/utils.ts new file mode 100644 index 000000000..fb5d6cbcf --- /dev/null +++ b/packages/fetch-sponsors/utils.ts @@ -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; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65f765108..2a379318f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/components/sponsors/GitHub/_ContributorsMain.astro b/src/components/sponsors/GitHub/Contributors.astro similarity index 71% rename from src/components/sponsors/GitHub/_ContributorsMain.astro rename to src/components/sponsors/GitHub/Contributors.astro index d6d5c9e7e..2e68a14fc 100644 --- a/src/components/sponsors/GitHub/_ContributorsMain.astro +++ b/src/components/sponsors/GitHub/Contributors.astro @@ -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 diff --git a/src/components/sponsors/GitHub/Main.astro b/src/components/sponsors/GitHub/Main.astro deleted file mode 100644 index d46e1adac..000000000 --- a/src/components/sponsors/GitHub/Main.astro +++ /dev/null @@ -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 { - 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 && ( -
- {gitHubSponsors.map((sponsor) => ( -
- {sponsor.avatarUrl && ( - - {sponsor.name} - - )} -
- ))} -
- ) -} - - diff --git a/src/components/sponsors/GitHub/Sponsors.astro b/src/components/sponsors/GitHub/Sponsors.astro new file mode 100644 index 000000000..42b1e0aa0 --- /dev/null +++ b/src/components/sponsors/GitHub/Sponsors.astro @@ -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 && ( +
+ {gitHubSponsors.map((sponsor: GitHubSponsor) => ( +
+ {sponsor.avatarUrl && ( + + {sponsor.name} + + )} +
+ ))} +
+ ) +} + + diff --git a/src/components/sponsors/OpenCollective/Main.astro b/src/components/sponsors/OpenCollective/Collective.astro similarity index 69% rename from src/components/sponsors/OpenCollective/Main.astro rename to src/components/sponsors/OpenCollective/Collective.astro index f02b52aab..ebeb69045 100644 --- a/src/components/sponsors/OpenCollective/Main.astro +++ b/src/components/sponsors/OpenCollective/Collective.astro @@ -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;

Platinum Sponsors (${PLATINUM_THRESHOLD.toLocaleString()}+)

@@ -56,8 +53,8 @@ const totalBronzeSponsors = sponsorsByTier.bronze.length;

Gold Sponsors (${GOLD_THRESHOLD.toLocaleString()}+)

@@ -69,9 +66,9 @@ const totalBronzeSponsors = sponsorsByTier.bronze.length;

Silver Sponsors (${SILVER_THRESHOLD.toLocaleString()}+)

@@ -85,7 +82,7 @@ const totalBronzeSponsors = sponsorsByTier.bronze.length;

Bronze Sponsors