feat(scripts): generate README sponsor tiers from Open Collective

updateSponsors.mts now fetches active recurring contributions from the
Open Collective GraphQL API, groups them by tier and rewrites the tier
sections in README.md between new markers, then propagates the block to
the package READMEs as before. Use --skip-fetch to only propagate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRsauu8mwJpB9y88vMMQ7n
This commit is contained in:
Claude
2026-10-14 12:08:14 +00:00
parent d672fc43db
commit f97c53d756
14 changed files with 244 additions and 6 deletions
+4
View File
@@ -90,6 +90,8 @@ Thank you to all the people who contributed to Lucide!
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="docs/public/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="docs/public/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -103,6 +105,8 @@ Thank you to all the people who contributed to Lucide!
<a href="https://www.fina.money/"><img src="docs/public/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -73,6 +73,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -86,6 +88,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -70,6 +70,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -83,6 +85,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -78,6 +78,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -91,6 +93,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -69,6 +69,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -69,6 +69,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -69,6 +69,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -69,6 +69,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -83,6 +83,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -96,6 +98,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -79,6 +79,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -92,6 +94,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -71,6 +71,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -84,6 +86,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -71,6 +71,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -84,6 +86,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+4
View File
@@ -69,6 +69,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Sponsor tiers start)
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="https://lucide.dev/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
@@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
[//]: <> (Sponsor tiers end)
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
+192 -6
View File
@@ -1,10 +1,193 @@
import { readdirSync } from 'node:fs';
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { existsSync, readFileSync, writeFileSync } from 'fs';
function updateSponsors() {
const packagesPath = join(import.meta.dirname, '../packages');
const mainReadme = readFileSync(join(import.meta.dirname, '../README.md'), 'utf8');
const COLLECTIVE_SLUG = 'lucide-icons';
const OPEN_COLLECTIVE_API = 'https://api.opencollective.com/graphql/v2';
const rootPath = join(import.meta.dirname, '..');
const mainReadmePath = join(rootPath, 'README.md');
const sponsorLogosPath = join(rootPath, 'docs/public/sponsors');
const tiersStartMarker = '[//]: <> (Sponsor tiers start)';
const tiersEndMarker = '[//]: <> (Sponsor tiers end)';
/**
* Headings for the Open Collective tiers, keyed by tier slug.
* Tiers that are not listed here fall back to their Open Collective name.
*/
const TIER_TITLES: Record<string, string> = {
'hero-backer': 'Hero backers 🦸',
'awesome-backer': 'Awesome backers 🍺',
backer: 'Backers ☕',
};
interface Account {
slug: string;
name: string;
website: string | null;
imageUrl: string | null;
description: string | null;
}
interface Tier {
slug: string;
name: string;
amount: { valueInCents: number | null } | null;
}
interface Order {
tier: Pick<Tier, 'slug'> | null;
fromAccount: Account;
}
interface CollectiveResponse {
data?: {
account: {
tiers: { nodes: Tier[] };
orders: { nodes: Order[] };
} | null;
};
errors?: { message: string }[];
}
const query = /* GraphQL */ `
query Sponsors($slug: String!) {
account(slug: $slug) {
... on AccountWithContributions {
tiers(limit: 100) {
nodes {
slug
name
amount {
valueInCents
}
}
}
}
orders(filter: INCOMING, status: [ACTIVE], limit: 1000) {
nodes {
tier {
slug
}
fromAccount {
slug
name
website
imageUrl
description
}
}
}
}
}
`;
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;
}
const escapeHtml = (value: string) =>
value
.replaceAll('&', '&amp;')
.replaceAll('"', '&quot;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
function getLogo({ slug, imageUrl }: Account) {
const localLogo = ['svg', 'png'].find((extension) =>
existsSync(join(sponsorLogosPath, `${slug}.${extension}`)),
);
if (localLogo) return `docs/public/sponsors/${slug}.${localLogo}`;
return imageUrl ?? `https://images.opencollective.com/${slug}/logo/256.png`;
}
function renderSponsor(account: Account) {
const href = account.website || `https://opencollective.com/${account.slug}`;
const alt = account.description ? `${account.name} – ${account.description}` : account.name;
return `<a href="${escapeHtml(href)}"><img src="${escapeHtml(getLogo(account))}" width="180" alt="${escapeHtml(alt)}" /></a>`;
}
function renderTiers({ tiers, orders }: Awaited<ReturnType<typeof fetchCollective>>) {
const sponsorsByTier = new Map<string, Map<string, Account>>();
orders.nodes.forEach(({ tier, fromAccount }) => {
if (!tier) return;
if (!sponsorsByTier.has(tier.slug)) sponsorsByTier.set(tier.slug, new Map());
sponsorsByTier.get(tier.slug)!.set(fromAccount.slug, fromAccount);
});
return [...tiers.nodes]
.sort((a, b) => (b.amount?.valueInCents ?? 0) - (a.amount?.valueInCents ?? 0))
.flatMap((tier) => {
const sponsors = [...(sponsorsByTier.get(tier.slug)?.values() ?? [])];
if (!sponsors.length) return [];
if (!TIER_TITLES[tier.slug]) {
console.warn(`No title configured for tier "${tier.slug}", using "${tier.name}"`);
}
const sponsorLinks = sponsors
.sort((a, b) => a.name.localeCompare(b.name))
.map(renderSponsor)
.join('\n');
return `### ${TIER_TITLES[tier.slug] ?? tier.name}\n\n${sponsorLinks}`;
})
.join('\n\n');
}
async function updateReadmeTiers() {
const mainReadme = readFileSync(mainReadmePath, 'utf8');
const startIndex = mainReadme.indexOf(tiersStartMarker);
const endIndex = mainReadme.indexOf(tiersEndMarker);
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
throw new Error(`Could not find "${tiersStartMarker}" and "${tiersEndMarker}" in README.md`);
}
const tiersMarkup = renderTiers(await fetchCollective());
writeFileSync(
mainReadmePath,
[
mainReadme.slice(0, startIndex + tiersStartMarker.length),
tiersMarkup,
mainReadme.slice(endIndex),
]
.filter(Boolean)
.join('\n\n'),
);
console.log(`Updated sponsor tiers in ${mainReadmePath}`);
}
function updatePackageSponsors() {
const packagesPath = join(rootPath, 'packages');
const mainReadme = readFileSync(mainReadmePath, 'utf8');
const sponsorsRegex = /(\[\/\/]: <> \(Sponsors\).+)$/s;
const sponsorsBlock = sponsorsRegex.exec(mainReadme);
if (sponsorsBlock?.[1]) {
@@ -31,4 +214,7 @@ function updateSponsors() {
}
}
updateSponsors();
if (!process.argv.includes('--skip-fetch')) {
await updateReadmeTiers();
}
updatePackageSponsors();