Files
lucide/docs/scripts/writeSponsorsData.mts
T
EricandClaude Opus 5.5 50db2b2a39 feat(docs): add navbar sponsor button, more sponsor stats and tier links
- Replace the Sponsor nav text link with a dedicated heart button in the
  navbar, shown icon-only at tablet widths where the menu is crowded
- Show weekly npm downloads across all Lucide packages and the all-time
  Open Collective sponsor count in the sponsor page hero
- Add tierLinks.ts for direct Open Collective and GitHub Sponsors tier
  checkout links, falling back to the general pages while empty

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:21:16 +01:00

170 lines
4.7 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import { loadEnvFile } from 'node:process';
const COLLECTIVE_SLUG = 'lucide-icons';
const OPEN_COLLECTIVE_API = 'https://api.opencollective.com/graphql/v2';
const currentDir = process.cwd();
const dataDirectory = path.resolve(currentDir, '.vitepress/data');
const sponsorLogosPath = path.resolve(currentDir, 'public/sponsors');
const outputPath = path.resolve(dataDirectory, 'sponsorsData.json');
try {
// Load environment variables from .env file, if it exists.
loadEnvFile(`${currentDir}/.env`);
} catch {
// No .env file, so rely on the environment as-is.
}
interface Account {
slug: string;
name: string;
website: string | null;
imageUrl: string | null;
}
interface Order {
amount: { valueInCents: number | null } | null;
frequency: 'MONTHLY' | 'YEARLY' | 'ONETIME' | null;
fromAccount: Account;
}
interface CollectiveResponse {
data?: {
account: {
stats: { contributorsCount: number | null } | null;
orders: { nodes: Order[] };
} | null;
};
errors?: { message: string }[];
}
export interface Sponsor {
slug: string;
name: string;
website: string;
/** Logo for the logo tiers, preferring a local file in `public/sponsors`. */
image: string;
/** Square avatar for the backer and supporter tiers. */
avatar: string;
/** Recurring contribution normalised to a monthly amount, in the collective's currency. */
monthlyAmount: number;
}
const query = /* GraphQL */ `
query Sponsors($slug: String!) {
account(slug: $slug) {
stats {
contributorsCount
}
orders(filter: INCOMING, status: [ACTIVE], limit: 1000) {
nodes {
amount {
valueInCents
}
frequency
fromAccount {
slug
name
website
imageUrl
}
}
}
}
}
`;
async function fetchCollective() {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (process.env.OPENCOLLECTIVE_TOKEN) {
headers['Personal-Token'] = process.env.OPENCOLLECTIVE_TOKEN;
}
const response = await fetch(OPEN_COLLECTIVE_API, {
method: 'POST',
headers,
body: JSON.stringify({ query, variables: { slug: COLLECTIVE_SLUG } }),
});
if (!response.ok) {
throw new Error(`Open Collective API responded with ${response.status} ${response.statusText}`);
}
const { data, errors } = (await response.json()) as CollectiveResponse;
if (errors?.length) {
throw new Error(
`Open Collective API errors: ${errors.map(({ message }) => message).join(', ')}`,
);
}
if (!data?.account) {
throw new Error(`Could not find Open Collective account "${COLLECTIVE_SLUG}"`);
}
return data.account;
}
function getLogo({ slug, imageUrl }: Account) {
const localLogo = ['svg', 'png'].find((extension) =>
fs.existsSync(path.join(sponsorLogosPath, `${slug}.${extension}`)),
);
if (localLogo) return `/sponsors/${slug}.${localLogo}`;
return imageUrl ?? `https://images.opencollective.com/${slug}/logo/256.png`;
}
function getMonthlyAmount({ amount, frequency }: Order) {
const value = (amount?.valueInCents ?? 0) / 100;
return frequency === 'YEARLY' ? value / 12 : value;
}
function toSponsors(orders: Order[]) {
const sponsors = new Map<string, Sponsor>();
orders.forEach((order) => {
const { fromAccount } = order;
const existing = sponsors.get(fromAccount.slug);
if (existing) {
existing.monthlyAmount += getMonthlyAmount(order);
return;
}
sponsors.set(fromAccount.slug, {
slug: fromAccount.slug,
name: fromAccount.name,
website: fromAccount.website || `https://opencollective.com/${fromAccount.slug}`,
image: getLogo(fromAccount),
avatar:
fromAccount.imageUrl ??
`https://images.opencollective.com/${fromAccount.slug}/avatar/128.png`,
monthlyAmount: getMonthlyAmount(order),
});
});
return [...sponsors.values()]
.map((sponsor) => ({
...sponsor,
monthlyAmount: Math.round(sponsor.monthlyAmount * 100) / 100,
}))
.sort((a, b) => b.monthlyAmount - a.monthlyAmount || a.name.localeCompare(b.name));
}
let sponsors: Sponsor[] = [];
/** Everyone who ever contributed financially, including past and one-time contributors. */
let allTimeCount: number | null = null;
try {
const collective = await fetchCollective();
sponsors = toSponsors(collective.orders.nodes);
allTimeCount = collective.stats?.contributorsCount ?? null;
} catch (error) {
console.warn(`Could not fetch sponsors, writing an empty list. ${error}`);
}
fs.mkdirSync(dataDirectory, { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify({ allTimeCount, sponsors }, null, 2), 'utf-8');
console.log(`Wrote ${sponsors.length} sponsors to ${outputPath}`);