UNPKG

@simon_he/sponsorkit

Version:

Toolkit for generating sponsors images

295 lines (287 loc) 11.3 kB
'use strict'; const process = require('node:process'); const yargs = require('yargs'); const node_path = require('node:path'); const fs = require('fs-extra'); const consola = require('consola'); const c = require('picocolors'); const index = require('./index.cjs'); require('node:buffer'); const ofetch = require('ofetch'); require('image-data-uri'); require('sharp'); require('unconfig'); require('dotenv'); require('node-html-parser'); require('node:crypto'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } const process__default = /*#__PURE__*/_interopDefaultCompat(process); const yargs__default = /*#__PURE__*/_interopDefaultCompat(yargs); const fs__default = /*#__PURE__*/_interopDefaultCompat(fs); const c__default = /*#__PURE__*/_interopDefaultCompat(c); const version = "0.0.10"; async function customAddUser(users) { const customData = []; await Promise.all(users.map(({ user, monthlyDollars }) => { return new Promise((resolve) => { ofetch.$fetch(`https://api.github.com/users/${user}`, { responseType: "json" }).then(async (data) => { customData.push({ sponsor: { type: "User", name: user, login: user, avatarUrl: data.avatar_url, linkUrl: `https://github.com/${user}` }, monthlyDollars }); resolve(customData); }).catch((error) => { console.error("\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25:", error); resolve(customData); }); }); })); return customData; } function r(path) { return `./${node_path.relative(process__default.cwd(), path)}`; } async function run(inlineConfig, t = consola.consola) { t.log(` ${c__default.magenta(c__default.bold("SponsorKit"))} ${c__default.dim(`v${version}`)} `); const config = await index.loadConfig(inlineConfig); const type = config.type || "all"; const dir = node_path.resolve(process__default.cwd(), config.outputDir); const cacheFile = node_path.resolve(dir, config.cacheFile); const providers = index.resolveProviders(config.providers || index.guessProviders(config)); t.info("Composing SVG..."); let allSponsors = await getAllSponsors(); if (type === "all") { generateTier(); generateCircle(); } else if (type === "tiers") { generateTier(); } else if (type === "circle") { generateCircle(); } async function generateTier() { const composer = new index.SvgComposer(config); await (config.customComposer || defaultComposer)(composer, allSponsors, config); let svg = composer.generateSvg(); svg = await config.onSvgGenerated?.(svg) || svg; if (config.formats?.includes("svg")) { const path = node_path.join(dir, `${config.name}.svg`); await fs__default.writeFile(path, svg, "utf-8"); t.success(`Wrote to ${r(path)}`); } if (config.formats?.includes("png")) { const path = node_path.join(dir, `${config.name}.png`); await fs__default.writeFile(path, await index.svgToPng(svg)); t.success(`Wrote to ${r(path)}`); } } async function generateCircle() { const { hierarchy, pack } = await import('d3-hierarchy'); const composer = new index.SvgComposer(config); const amountMax = Math.max(...allSponsors.map((sponsor) => sponsor.monthlyDollars)); const { radiusMax = 300, radiusMin = 10, radiusPast = 5, weightInterop = defaultInterop } = config.circles || {}; function defaultInterop(sponsor) { return sponsor.monthlyDollars < 0 ? radiusPast : lerp(radiusMin, radiusMax, (Math.max(0.1, sponsor.monthlyDollars || 0) / amountMax) ** 0.9); } if (!config.includePastSponsors) allSponsors = allSponsors.filter((sponsor) => sponsor.monthlyDollars > 0); const root = hierarchy({ ...allSponsors[0], children: allSponsors, id: "root" }).sum((d) => weightInterop(d, amountMax)).sort((a, b) => (b.value || 0) - (a.value || 0)); const p = pack(); p.size([config.width, config.width]); p.padding(config.width / 400); const circles = p(root).descendants().slice(1); for (const circle of circles) { composer.addRaw(index.generateBadge( circle.x - circle.r, circle.y - circle.r, await getRoundedAvatars(circle.data.sponsor), { name: false, boxHeight: circle.r * 2, boxWidth: circle.r * 2, avatar: { size: circle.r * 2 } } )); } composer.height = config.width; const svg = composer.generateSvg(); if (config.formats?.includes("svg")) { const path = node_path.join(dir, `${config.name}_circle.svg`); await fs__default.writeFile(path, svg, "utf-8"); t.success(`Wrote to ${r(path)}`); } if (config.formats?.includes("png")) { const path = node_path.join(dir, `${config.name}_circle.png`); await fs__default.writeFile(path, await index.svgToPng(svg)); t.success(`Wrote to ${r(path)}`); } } async function getAllSponsors() { let allSponsors2 = []; if (!fs__default.existsSync(cacheFile) || config.force) { for (const i of providers) { t.info(`Fetching sponsorships from ${i.name}...`); let sponsors = []; try { sponsors = await i.fetchSponsors(config); } catch (error) { } sponsors.forEach((s) => s.provider = i.name); sponsors = await config.onSponsorsFetched?.(sponsors, i.name) || sponsors; t.success(`${sponsors.length} sponsorships fetched from ${i.name}`); allSponsors2.push(...sponsors); } if (config.customGithubUser) { const customSponsors = await customAddUser(config.customGithubUser); await index.resolveAvatars(customSponsors, config.fallbackAvatar, t); allSponsors2.push(...customSponsors); } t.info("Resolving avatars..."); await index.resolveAvatars(allSponsors2, config.fallbackAvatar, t); t.success("Avatars resolved"); await fs__default.ensureDir(node_path.dirname(cacheFile)); await fs__default.writeJSON(cacheFile, allSponsors2, { spaces: 2 }); } else { allSponsors2 = await fs__default.readJSON(cacheFile); if (config.customGithubUser) { const cacheUsers = allSponsors2.map((s) => ({ user: s.sponsor.name, monthlyDollars: s.monthlyDollars })); const isCached = (user) => cacheUsers.some((c2) => c2.user === user.user && c2.monthlyDollars === user.monthlyDollars); const isNeedUpdateMonthlyDollars = (user) => cacheUsers.some((c2) => c2.user === user.user && c2.monthlyDollars !== user.monthlyDollars); const filters = config.customGithubUser.filter((p) => p.user && !isCached(p)); allSponsors2 = allSponsors2.filter((s) => !isNeedUpdateMonthlyDollars({ user: s.sponsor.name, monthlyDollars: s.monthlyDollars })); const customSponsors = await customAddUser(filters); if (customSponsors.length > 0) { await index.resolveAvatars(customSponsors, config.fallbackAvatar, t); allSponsors2 = allSponsors2.filter((s) => !customSponsors.some((c2) => c2.sponsor.name === s.sponsor.name)); allSponsors2.push(...customSponsors); t.success(`Added new users: ${filters.map((i) => `[${i.user}]`).join(" ")} custom sponsorships`); await fs__default.writeJSON(cacheFile, allSponsors2, { spaces: 2 }); } else { t.success(`Loaded from cache ${r(cacheFile)}`); } } else { t.success(`Loaded from cache ${r(cacheFile)}`); } } allSponsors2.sort( (a, b) => b.monthlyDollars - a.monthlyDollars || Date.parse(b.createdAt) - Date.parse(a.createdAt) || (b.sponsor.login || b.sponsor.name).localeCompare(a.sponsor.login || a.sponsor.name) // ASC name ); await fs__default.ensureDir(dir); if (config.formats?.includes("json")) { const path = node_path.join(dir, `${config.name}.json`); await fs__default.writeJSON(path, allSponsors2, { spaces: 2 }); t.success(`Wrote to ${r(path)}`); } allSponsors2 = await config.onSponsorsReady?.(allSponsors2) || allSponsors2; if (config.filter) allSponsors2 = allSponsors2.filter((s) => config.filter(s, allSponsors2) !== false); if (!config.includePrivate) allSponsors2 = allSponsors2.filter((s) => s.privacyLevel !== "PRIVATE"); return allSponsors2; } } async function getRoundedAvatars(sponsor) { if (!sponsor.avatarBuffer || sponsor.type === "User") return sponsor; const data = index.base64ToArrayBuffer(sponsor.avatarBuffer); return { ...sponsor, avatarUrlHighRes: index.pngToDataUri(await index.round(data, 0.5, 120)), avatarUrlLowRes: index.pngToDataUri(await index.round(data, 0.5, 50)), avatarUrlMediumRes: index.pngToDataUri(await index.round(data, 0.5, 80)) }; } function lerp(a, b, t) { if (t < 0) return a; return a + (b - a) * t; } async function defaultComposer(composer, sponsors, config) { const tierPartitions = index.partitionTiers(sponsors, config.tiers); composer.addSpan(config.padding?.top ?? 20); tierPartitions.forEach(({ tier: t, sponsors: sponsors2 }) => { t.composeBefore?.(composer, sponsors2, config); if (t.compose) { t.compose(composer, sponsors2, config); } else { const preset = t.preset || index.presets.base; if (sponsors2.length && preset.avatar.size) { const paddingTop = t.padding?.top ?? 20; const paddingBottom = t.padding?.bottom ?? 10; if (paddingTop) composer.addSpan(paddingTop); if (t.title) { composer.addTitle(t.title).addSpan(5); } composer.addSponsorGrid(sponsors2, preset); if (paddingBottom) composer.addSpan(paddingBottom); } } t.composeAfter?.(composer, sponsors2, config); }); composer.addSpan(config.padding?.bottom ?? 20); } const cli = yargs__default(process__default.argv.slice(2)).scriptName("sponsors-svg").usage("$0 [args]").version(version).strict().showHelpOnFail(false).alias("h", "help").alias("v", "version"); cli.command( "*", "Generate", (args) => args.option("width", { alias: "w", type: "number", default: 800 }).option("fallbackAvatar", { type: "string", alias: "fallback" }).option("force", { alias: "f", default: false, type: "boolean" }).option("name", { type: "string" }).option("filter", { type: "string" }).option("outputDir", { type: "string", alias: ["o", "dir"] }).strict().help(), async (options) => { const config = options; if (options._[0]) config.outputDir = options._[0]; if (options.filter) config.filter = createFilterFromString(options.filter); await run(config); } ); cli.help().parse(); function createFilterFromString(template) { const [_, op, value] = template.split(/([<>=]+)/); const num = Number.parseInt(value); if (op === "<") return (s) => s.monthlyDollars < num; if (op === "<=") return (s) => s.monthlyDollars <= num; if (op === ">") return (s) => s.monthlyDollars > num; if (op === ">=") return (s) => s.monthlyDollars >= num; throw new Error(`Unable to parse filter template ${template}`); }