@simon_he/sponsorkit
Version:
Toolkit for generating sponsors images
295 lines (287 loc) • 11.3 kB
JavaScript
;
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}`);
}