From f97c53d75662e5a5de3ca902bf26cd1fbef05a87 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 12:08:14 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01VRsauu8mwJpB9y88vMMQ7n --- README.md | 4 + packages/angular/README.md | 4 + packages/astro/README.md | 4 + packages/icons/README.md | 4 + packages/lucide-preact/README.md | 4 + packages/lucide-react-native/README.md | 4 + packages/lucide-react/README.md | 4 + packages/lucide-solid/README.md | 4 + packages/lucide-static/README.md | 4 + packages/lucide/README.md | 4 + packages/solid/README.md | 4 + packages/svelte/README.md | 4 + packages/vue/README.md | 4 + scripts/updateSponsors.mts | 198 ++++++++++++++++++++++++- 14 files changed, 244 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a33e9026d..486e4b797 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ Thank you to all the people who contributed to Lucide! DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -103,6 +105,8 @@ Thank you to all the people who contributed to Lucide! Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/angular/README.md b/packages/angular/README.md index 7650362dc..1bed72b40 100644 --- a/packages/angular/README.md +++ b/packages/angular/README.md @@ -73,6 +73,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -86,6 +88,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/astro/README.md b/packages/astro/README.md index 8898336f1..89ba6caa7 100644 --- a/packages/astro/README.md +++ b/packages/astro/README.md @@ -70,6 +70,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -83,6 +85,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/icons/README.md b/packages/icons/README.md index 1cd8df31d..7b63fa127 100644 --- a/packages/icons/README.md +++ b/packages/icons/README.md @@ -78,6 +78,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -91,6 +93,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/lucide-preact/README.md b/packages/lucide-preact/README.md index fda48ad2b..64de4da12 100644 --- a/packages/lucide-preact/README.md +++ b/packages/lucide-preact/README.md @@ -69,6 +69,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/lucide-react-native/README.md b/packages/lucide-react-native/README.md index c5a9034cb..a15be95dd 100644 --- a/packages/lucide-react-native/README.md +++ b/packages/lucide-react-native/README.md @@ -69,6 +69,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/lucide-react/README.md b/packages/lucide-react/README.md index a034bcc0f..c8102f32f 100644 --- a/packages/lucide-react/README.md +++ b/packages/lucide-react/README.md @@ -69,6 +69,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/lucide-solid/README.md b/packages/lucide-solid/README.md index b006549df..7df7babce 100644 --- a/packages/lucide-solid/README.md +++ b/packages/lucide-solid/README.md @@ -69,6 +69,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/lucide-static/README.md b/packages/lucide-static/README.md index a13c57f3e..48e2e03f5 100644 --- a/packages/lucide-static/README.md +++ b/packages/lucide-static/README.md @@ -83,6 +83,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -96,6 +98,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/lucide/README.md b/packages/lucide/README.md index 0e3bb9433..819d378f5 100644 --- a/packages/lucide/README.md +++ b/packages/lucide/README.md @@ -79,6 +79,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -92,6 +94,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/solid/README.md b/packages/solid/README.md index 242f974b8..5a30f80bc 100644 --- a/packages/solid/README.md +++ b/packages/solid/README.md @@ -71,6 +71,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -84,6 +86,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/svelte/README.md b/packages/svelte/README.md index 316ee6efc..798ba91ee 100644 --- a/packages/svelte/README.md +++ b/packages/svelte/README.md @@ -71,6 +71,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -84,6 +86,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/packages/vue/README.md b/packages/vue/README.md index fa5f6c610..86978f32d 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -69,6 +69,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen DigitalOcean Referral Badge +[//]: <> (Sponsor tiers start) + ### Hero backers 🦸 Zephyr Cloud – From idea to prod: fast micro-frontend delivery! @@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen Fina Money – Modular Finance Tracker +[//]: <> (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). diff --git a/scripts/updateSponsors.mts b/scripts/updateSponsors.mts index 816fa109c..03c46143f 100644 --- a/scripts/updateSponsors.mts +++ b/scripts/updateSponsors.mts @@ -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 = { + '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 | 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 = { '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('&', '&') + .replaceAll('"', '"') + .replaceAll('<', '<') + .replaceAll('>', '>'); + +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 `${escapeHtml(alt)}`; +} + +function renderTiers({ tiers, orders }: Awaited>) { + const sponsorsByTier = new Map>(); + + 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();