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();