2026-10-21 11:20:37 +02:00
|
|
|
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
2026-01-22 11:27:39 +01:00
|
|
|
|
import { join } from 'node:path';
|
2026-11-02 12:21:25 +01:00
|
|
|
|
import { TIERS, type TierId } from '../docs/.vitepress/theme/components/sponsor/tiers.ts';
|
|
|
|
|
|
import {
|
|
|
|
|
|
type Account,
|
|
|
|
|
|
fetchCollective,
|
|
|
|
|
|
getCurrentSponsors,
|
|
|
|
|
|
getLocalLogos,
|
|
|
|
|
|
getOpenCollectiveLogo,
|
|
|
|
|
|
getWebsite,
|
|
|
|
|
|
} from '../docs/scripts/helpers/openCollective.mts';
|
2026-10-21 11:20:37 +02:00
|
|
|
|
|
|
|
|
|
|
const rootPath = join(import.meta.dirname, '..');
|
|
|
|
|
|
const mainReadmePath = join(rootPath, 'README.md');
|
|
|
|
|
|
|
|
|
|
|
|
const tiersStartMarker = '[//]: <> (Sponsor tiers start)';
|
|
|
|
|
|
const tiersEndMarker = '[//]: <> (Sponsor tiers end)';
|
2026-11-02 12:21:25 +01:00
|
|
|
|
const mainReadmeOnlyStartMarker = '[//]: <> (Main README only start)';
|
|
|
|
|
|
const mainReadmeOnlyEndMarker = '[//]: <> (Main README only end)';
|
2026-10-21 11:20:37 +02:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-11-02 12:21:25 +01:00
|
|
|
|
* How each sponsor tier shows up in the READMEs, following the tier perks in tiers.ts.
|
|
|
|
|
|
* Tiers that are not in the npm package READMEs are wrapped in the "Main README only" markers.
|
|
|
|
|
|
* Only tiers with `customLogo` use the logos in docs/public/sponsors, the others use their avatar.
|
2026-10-21 11:20:37 +02:00
|
|
|
|
*/
|
2026-11-02 12:21:25 +01:00
|
|
|
|
const README_TIERS: Record<
|
|
|
|
|
|
TierId,
|
|
|
|
|
|
{ title: string; logoHeight: number; inPackages: boolean; customLogo: boolean }
|
|
|
|
|
|
> = {
|
|
|
|
|
|
platinum: { title: 'Platinum 💎', logoHeight: 96, inPackages: true, customLogo: true },
|
|
|
|
|
|
gold: { title: 'Gold 🥇', logoHeight: 64, inPackages: true, customLogo: true },
|
|
|
|
|
|
bronze: { title: 'Bronze 🥉', logoHeight: 48, inPackages: true, customLogo: true },
|
|
|
|
|
|
backer: { title: 'Backers 🍺', logoHeight: 32, inPackages: false, customLogo: false },
|
|
|
|
|
|
supporter: { title: 'Supporters ☕️', logoHeight: 32, inPackages: false, customLogo: false },
|
2026-10-21 11:20:37 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const escapeHtml = (value: string) =>
|
|
|
|
|
|
value
|
|
|
|
|
|
.replaceAll('&', '&')
|
|
|
|
|
|
.replaceAll('"', '"')
|
|
|
|
|
|
.replaceAll('<', '<')
|
|
|
|
|
|
.replaceAll('>', '>');
|
|
|
|
|
|
|
2026-11-02 12:21:25 +01:00
|
|
|
|
/** Uses a `<picture>` to switch to the dark logo in dark mode, when there is one. */
|
|
|
|
|
|
function renderSponsor(account: Account, height: number, customLogo: boolean) {
|
|
|
|
|
|
const logos: { light?: string; dark?: string } = customLogo ? getLocalLogos(account.slug) : {};
|
|
|
|
|
|
const src = logos.light ? `docs/public/sponsors/${logos.light}` : getOpenCollectiveLogo(account);
|
2026-10-21 11:20:37 +02:00
|
|
|
|
const alt = account.description ? `${account.name} – ${account.description}` : account.name;
|
|
|
|
|
|
|
2026-11-02 12:21:25 +01:00
|
|
|
|
const image = `<img src="${escapeHtml(src)}" height="${height}" alt="${escapeHtml(alt)}" />`;
|
|
|
|
|
|
const logo = logos.dark
|
|
|
|
|
|
? `<picture><source media="(prefers-color-scheme: dark)" srcset="docs/public/sponsors/${escapeHtml(logos.dark)}" />${image}</picture>`
|
|
|
|
|
|
: image;
|
|
|
|
|
|
|
|
|
|
|
|
return `<a href="${escapeHtml(getWebsite(account))}">${logo}</a>`;
|
2026-10-21 11:20:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-02 12:21:25 +01:00
|
|
|
|
function renderTiers({ orders }: Awaited<ReturnType<typeof fetchCollective>>) {
|
|
|
|
|
|
const sponsorsByTier = Map.groupBy(getCurrentSponsors(orders.nodes), ({ tier }) => tier);
|
2026-10-21 11:20:37 +02:00
|
|
|
|
|
2026-11-02 12:21:25 +01:00
|
|
|
|
// Highest tier first, so Platinum sponsors are at the top.
|
|
|
|
|
|
const renderTierGroup = (inPackages: boolean) =>
|
|
|
|
|
|
[...TIERS]
|
|
|
|
|
|
.reverse()
|
|
|
|
|
|
.filter(({ id }) => README_TIERS[id].inPackages === inPackages)
|
|
|
|
|
|
.flatMap(({ id }) => {
|
|
|
|
|
|
const sponsors = sponsorsByTier.get(id) ?? [];
|
|
|
|
|
|
if (!sponsors.length) return [];
|
2026-10-21 11:20:37 +02:00
|
|
|
|
|
2026-11-02 12:21:25 +01:00
|
|
|
|
const { title, logoHeight, customLogo } = README_TIERS[id];
|
|
|
|
|
|
const sponsorLinks = sponsors
|
|
|
|
|
|
.map(({ account }) => account)
|
|
|
|
|
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
|
|
|
|
.map((account) => renderSponsor(account, logoHeight, customLogo))
|
|
|
|
|
|
// READMEs strip `style`, so non-breaking spaces are used to space out the logos.
|
|
|
|
|
|
.join('\n \n');
|
2026-10-21 11:20:37 +02:00
|
|
|
|
|
2026-11-02 12:21:25 +01:00
|
|
|
|
return `### ${title}\n\n${sponsorLinks}`;
|
|
|
|
|
|
})
|
|
|
|
|
|
.join('\n\n');
|
2026-10-21 11:20:37 +02:00
|
|
|
|
|
2026-11-02 12:21:25 +01:00
|
|
|
|
const packageTiers = renderTierGroup(true);
|
|
|
|
|
|
const mainReadmeOnlyTiers = renderTierGroup(false);
|
2026-10-21 11:20:37 +02:00
|
|
|
|
|
2026-11-02 12:21:25 +01:00
|
|
|
|
return [
|
|
|
|
|
|
packageTiers,
|
|
|
|
|
|
mainReadmeOnlyTiers &&
|
|
|
|
|
|
[mainReadmeOnlyStartMarker, mainReadmeOnlyTiers, mainReadmeOnlyEndMarker].join('\n\n'),
|
|
|
|
|
|
]
|
|
|
|
|
|
.filter(Boolean)
|
2026-10-21 11:20:37 +02:00
|
|
|
|
.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');
|
2026-03-28 07:48:26 +01:00
|
|
|
|
const sponsorsRegex = /(\[\/\/]: <> \(Sponsors\).+)$/s;
|
2026-11-02 12:21:25 +01:00
|
|
|
|
const mainReadmeOnlyRegex =
|
|
|
|
|
|
/\[\/\/]: <> \(Main README only start\).*?\[\/\/]: <> \(Main README only end\)\s*/gs;
|
2026-03-28 07:48:26 +01:00
|
|
|
|
const sponsorsBlock = sponsorsRegex.exec(mainReadme);
|
|
|
|
|
|
if (sponsorsBlock?.[1]) {
|
2026-11-02 12:21:25 +01:00
|
|
|
|
const packageSponsorsBlock = sponsorsBlock[1]
|
|
|
|
|
|
.replace(mainReadmeOnlyRegex, '')
|
|
|
|
|
|
.replaceAll('="docs/public/', '="https://lucide.dev/');
|
2026-01-22 11:27:39 +01:00
|
|
|
|
readdirSync(packagesPath).forEach((packagePath) => {
|
|
|
|
|
|
if (['lucide-figma', 'shared'].includes(packagePath) || packagePath.startsWith('.')) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const readmePath = join(packagesPath, packagePath, 'README.md');
|
|
|
|
|
|
if (existsSync(readmePath)) {
|
|
|
|
|
|
const readmeContent = readFileSync(readmePath, 'utf8');
|
2026-03-28 07:48:26 +01:00
|
|
|
|
if (sponsorsRegex.test(readmeContent)) {
|
|
|
|
|
|
writeFileSync(readmePath, readmeContent.replace(sponsorsRegex, packageSponsorsBlock));
|
|
|
|
|
|
console.log(`Upgraded Sponsors block in ${readmePath}`);
|
2026-01-22 11:27:39 +01:00
|
|
|
|
} else {
|
2026-11-02 12:21:25 +01:00
|
|
|
|
console.error(`Could not find Sponsors block in ${readmePath}`);
|
2026-01-22 11:27:39 +01:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn(`No readme found at ${readmePath}`);
|
|
|
|
|
|
}
|
2026-11-02 12:21:25 +01:00
|
|
|
|
});
|
2026-01-22 11:27:39 +01:00
|
|
|
|
} else {
|
2026-11-02 12:21:25 +01:00
|
|
|
|
console.error('Could not identify Sponsors block');
|
2026-01-22 11:27:39 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-21 11:20:37 +02:00
|
|
|
|
if (!process.argv.includes('--skip-fetch')) {
|
|
|
|
|
|
await updateReadmeTiers();
|
|
|
|
|
|
}
|
|
|
|
|
|
updatePackageSponsors();
|