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!
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -103,6 +105,8 @@ Thank you to all the people who contributed to Lucide!
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -86,6 +88,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -83,6 +85,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -91,6 +93,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -96,6 +98,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -92,6 +94,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -84,6 +86,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -84,6 +86,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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
+[//]: <> (Sponsor tiers start)
+
### Hero backers 🦸
@@ -82,6 +84,8 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
+[//]: <> (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 `
`;
+}
+
+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();