diff --git a/bun.lockb b/bun.lockb index c9b91253c..5a510450c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/docs/astro.config.ts b/packages/docs/astro.config.ts index c60e89b8f..7b0f7e197 100644 --- a/packages/docs/astro.config.ts +++ b/packages/docs/astro.config.ts @@ -25,6 +25,9 @@ export default defineConfig({ remarkPlugins: [fixInternalLinks, transformDirectives, remarkDirective], rehypePlugins: [rehypeHeadingIds, rehypeAutolinkHeadings], }, + experimental: { + svg: true, + }, integrations: [ starlight({ title: 'Knip', diff --git a/packages/docs/package.json b/packages/docs/package.json index 809628e1e..25c2ee679 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -30,6 +30,7 @@ "devDependencies": { "@astrojs/check": "0.9.4", "@astrojs/markdown-remark": "^6.1.0", + "@octokit/graphql": "^8.2.0", "@types/mdast": "4.0.4", "@types/unist": "3.0.3", "astro": "5.2.5", diff --git a/packages/docs/scripts/get-monthly-sponsorships-github.ts b/packages/docs/scripts/get-monthly-sponsorships-github.ts new file mode 100644 index 000000000..81beab80b --- /dev/null +++ b/packages/docs/scripts/get-monthly-sponsorships-github.ts @@ -0,0 +1,101 @@ +import { graphql } from '@octokit/graphql'; + +const START_DATE = new Date('2023-11-01'); + +interface SponsorActivity { + action: 'NEW_SPONSORSHIP' | 'CANCELLED_SPONSORSHIP'; + timestamp: string; + sponsorsTier: { + monthlyPriceInDollars: number; + isOneTime: boolean; + }; + sponsor: { + login: string; + }; +} + +interface GraphQLResponse { + viewer: { + sponsorsActivities: { + nodes: SponsorActivity[]; + }; + }; +} + +const getMonthlyTotals = async (token: string) => { + const { viewer } = await graphql({ + query: ` + query { + viewer { + sponsorsActivities(first: 100, period: ALL) { + nodes { + action + timestamp + sponsorsTier { + monthlyPriceInDollars + isOneTime + } + sponsor { + ... on User { login } + ... on Organization { login } + } + } + } + } + } + `, + headers: { + authorization: `token ${token}`, + }, + }); + + const activities = [...viewer.sponsorsActivities.nodes].sort( + (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() + ); + + const activeRecurring = new Map(); + const monthlyTotals = new Map(); + const now = new Date(); + + for (let d = new Date(START_DATE); d <= now; d.setMonth(d.getMonth() + 1)) { + monthlyTotals.set(d.toISOString().substring(0, 7), 0); + } + + for (const activity of activities) { + const { action, sponsor, sponsorsTier, timestamp } = activity; + const amount = sponsorsTier?.monthlyPriceInDollars || 0; + const monthYear = new Date(timestamp).toISOString().substring(0, 7); + + if (sponsorsTier?.isOneTime) { + if (action === 'NEW_SPONSORSHIP' && monthYear >= START_DATE.toISOString().substring(0, 7)) { + monthlyTotals.set(monthYear, (monthlyTotals.get(monthYear) || 0) + amount); + } + } else { + if (action === 'NEW_SPONSORSHIP') activeRecurring.set(sponsor.login, amount); + else if (action === 'CANCELLED_SPONSORSHIP') activeRecurring.delete(sponsor.login); + const recurringTotal = Array.from(activeRecurring.values()).reduce((sum, a) => sum + a, 0); + for (const [month] of monthlyTotals) if (month >= monthYear) monthlyTotals.set(month, recurringTotal); + } + } + + return monthlyTotals; +}; + +const main = async () => { + const token = process.env.GITHUB_TOKEN; + if (!token) { + throw new Error('GITHUB_TOKEN environment variable is not set'); + } + const monthlyData = await getMonthlyTotals(token); + + let grandTotal = 0; + + for (const [month, amount] of [...monthlyData.entries()].sort()) { + console.log(`${month} ${amount}`); + grandTotal += amount; + } + + console.log(`\nGrand total: ${grandTotal} (${monthlyData.size} months)`); +}; + +main().catch(console.error); diff --git a/packages/docs/scripts/get-monthly-sponsorships-opencollective.ts b/packages/docs/scripts/get-monthly-sponsorships-opencollective.ts new file mode 100644 index 000000000..486cac1ee --- /dev/null +++ b/packages/docs/scripts/get-monthly-sponsorships-opencollective.ts @@ -0,0 +1,119 @@ +import { graphql } from '@octokit/graphql'; + +const START_DATE = new Date('2023-11-01'); +const RATE_EUR_TO_USD = 1.08; + +interface Transaction { + id: string; + type: string; + kind: string; + amount: { + value: number; + currency: string; + }; + createdAt: string; + fromAccount: { + name: string; + }; +} + +interface Expense { + id: string; + amount: number; + currency: string; + createdAt: string; + type: string; + status: string; +} + +interface GraphQLResponse { + account: { + transactions: { + nodes: Transaction[]; + }; + }; + expenses: { + nodes: Expense[]; + }; +} + +const getMonthlyTotals = async (token: string): Promise> => { + const { account, expenses } = await graphql({ + query: ` + query { + account(slug: "knip") { + transactions(type: CREDIT) { + nodes { + id + type + kind + amount { + value + currency + } + createdAt + fromAccount { + name + } + } + } + } + expenses(fromAccount: { slug: "webpro" }) { + nodes { + id + amount + currency + createdAt + type + status + } + } + } + `, + url: 'https://api.opencollective.com/graphql/v2', + headers: { + 'Api-Key': token, + Accept: 'application/json', + }, + }); + + const monthlyTotals = new Map(); + const now = new Date(); + + for (let d = new Date(START_DATE); d <= now; d.setMonth(d.getMonth() + 1)) { + monthlyTotals.set(d.toISOString().substring(0, 7), 0); + } + + for (const transaction of account.transactions.nodes) { + const month = new Date(transaction.createdAt).toISOString().substring(0, 7); + const amount = Math.round(transaction.amount.value); + monthlyTotals.set(month, (monthlyTotals.get(month) || 0) + amount); + } + + for (const expense of expenses.nodes) { + const month = new Date(expense.createdAt).toISOString().substring(0, 7); + const amount = + expense.currency === 'EUR' + ? Math.round((expense.amount / 100) * RATE_EUR_TO_USD) + : Math.round(expense.amount / 100); + monthlyTotals.set(month, (monthlyTotals.get(month) || 0) + amount); + } + + return monthlyTotals; +}; + +const main = async () => { + const token = process.env.OPENCOLLECTIVE_TOKEN; + if (!token) throw new Error('OPENCOLLECTIVE_TOKEN is not set'); + const monthlyData = await getMonthlyTotals(token); + + let grandTotal = 0; + for (const [month, amount] of [...monthlyData.entries()].sort()) { + console.log(`${month} ${amount}`); + grandTotal += amount; + } + + console.log(`\nGrand total: ${grandTotal} (${monthlyData.size} months)`); +}; + +main().catch(console.error); diff --git a/packages/docs/src/assets/venz-chart.svg b/packages/docs/src/assets/venz-chart.svg new file mode 100644 index 000000000..fb18fb86f --- /dev/null +++ b/packages/docs/src/assets/venz-chart.svg @@ -0,0 +1,7 @@ +Chart for Knip Sponsorships02004006008001,0001,2001,4001,6001,8002,0002,2002,4002,6002,8002023-112023-122024-012024-022024-032024-042024-052024-062024-072024-082024-092024-102024-112024-122025-012025-02monthamount ($)GitHub SponsorsOpenCollective974239489189204304120420422927294102222223422220000075010103525254545145145145 \ No newline at end of file diff --git a/packages/docs/src/components/SponsorsChart.astro b/packages/docs/src/components/SponsorsChart.astro new file mode 100644 index 000000000..44fe507d6 --- /dev/null +++ b/packages/docs/src/components/SponsorsChart.astro @@ -0,0 +1,5 @@ +--- +import Chart from '../assets/venz-chart.svg'; +--- + + diff --git a/packages/docs/src/pages/sponsors.astro b/packages/docs/src/pages/sponsors.astro index c6e74267e..9a61dd94c 100644 --- a/packages/docs/src/pages/sponsors.astro +++ b/packages/docs/src/pages/sponsors.astro @@ -3,6 +3,7 @@ import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import Posts from '../components/Posts.astro'; import Projects from '../components/Projects.astro'; import SponsorsComponent from '../components/Sponsors.astro'; +import SponsorsChart from '../components/SponsorsChart.astro'; import SponsorsPast from '../components/SponsorsPast.astro'; --- @@ -38,13 +39,12 @@ import SponsorsPast from '../components/SponsorsPast.astro';

You or your company logo will be added to this page if you decide to support the project on an ongoing basis. Eternal gratitude to the - companies and people that are already supporters: + companies and people supporting the project!

@@ -57,6 +57,18 @@ import SponsorsPast from '../components/SponsorsPast.astro'; +

🧡 Monthly Overview

+ +

+ Overview of gross debit GitHub and OpenCollective sponsorships and invoices, + since it was openly asked for. The monthly aggregated average over the + charted period is $533, actual development started one year before that. + GitHub Sponsors is for my GitHub account, which has more repositories, but + in practice targets mostly Knip. +

+ + +

🧡 Past Sponsors