@lizardbyte/contribkit 2025.315.185528
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/bin/contribkit.mjs +2 -0
- package/dist/chunks/index.mjs +190 -0
- package/dist/chunks/index2.mjs +590 -0
- package/dist/chunks/index3.mjs +4905 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.mjs +399 -0
- package/dist/index.d.mts +494 -0
- package/dist/index.mjs +9 -0
- package/dist/shared/contribkit.QISHqB4U.mjs +1453 -0
- package/package.json +91 -0
package/dist/cli.d.mts
ADDED
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import cac from 'cac';
|
|
2
|
+
import { S as SvgComposer, i as generateBadge, p as partitionTiers, t as tierPresets, v as version, l as loadConfig, k as resolveProviders, j as guessProviders, r as resolveAvatars, q as outputFormats, s as svgToPng, g as svgToWebp } from './shared/contribkit.QISHqB4U.mjs';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import fsp from 'node:fs/promises';
|
|
5
|
+
import { resolve, dirname, relative, join } from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import c from 'ansis';
|
|
8
|
+
import { consola } from 'consola';
|
|
9
|
+
import { Buffer } from 'node:buffer';
|
|
10
|
+
import 'unconfig';
|
|
11
|
+
import 'dotenv';
|
|
12
|
+
import 'ofetch';
|
|
13
|
+
import 'sharp';
|
|
14
|
+
import 'node:crypto';
|
|
15
|
+
|
|
16
|
+
function notNullish(v) {
|
|
17
|
+
return v != null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stringifyCache(cache) {
|
|
21
|
+
return JSON.stringify(
|
|
22
|
+
cache,
|
|
23
|
+
(_key, value) => {
|
|
24
|
+
if (value && value.type === "Buffer" && Array.isArray(value.data)) {
|
|
25
|
+
return Buffer.from(value.data).toString("base64");
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
},
|
|
29
|
+
2
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
function parseCache(cache) {
|
|
33
|
+
return JSON.parse(cache, (key, value) => {
|
|
34
|
+
if (key === "avatarBuffer") {
|
|
35
|
+
return Buffer.from(value, "base64");
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const circlesRenderer = {
|
|
42
|
+
name: "contribkit:circles",
|
|
43
|
+
async renderSVG(config, sponsors) {
|
|
44
|
+
const { hierarchy, pack } = await import('./chunks/index2.mjs');
|
|
45
|
+
const composer = new SvgComposer(config);
|
|
46
|
+
const amountMax = Math.max(...sponsors.map((sponsor) => sponsor.monthlyDollars));
|
|
47
|
+
const {
|
|
48
|
+
radiusMax = 300,
|
|
49
|
+
radiusMin = 10,
|
|
50
|
+
radiusPast = 5,
|
|
51
|
+
weightInterop = defaultInterop
|
|
52
|
+
} = config.circles || {};
|
|
53
|
+
function defaultInterop(sponsor) {
|
|
54
|
+
return sponsor.monthlyDollars < 0 ? radiusPast : lerp(radiusMin, radiusMax, (Math.max(0.1, sponsor.monthlyDollars || 0) / amountMax) ** 0.9);
|
|
55
|
+
}
|
|
56
|
+
if (!config.includePastSponsors)
|
|
57
|
+
sponsors = sponsors.filter((sponsor) => sponsor.monthlyDollars > 0);
|
|
58
|
+
const root = hierarchy({ ...sponsors[0], children: sponsors, id: "root" }).sum((d) => weightInterop(d, amountMax)).sort((a, b) => (b.value || 0) - (a.value || 0));
|
|
59
|
+
const p = pack();
|
|
60
|
+
p.size([config.width, config.width]);
|
|
61
|
+
p.padding(config.width / 400);
|
|
62
|
+
const circles = p(root).descendants().slice(1);
|
|
63
|
+
for (const circle of circles) {
|
|
64
|
+
composer.addRaw(await generateBadge(
|
|
65
|
+
circle.x - circle.r,
|
|
66
|
+
circle.y - circle.r,
|
|
67
|
+
circle.data.sponsor,
|
|
68
|
+
{
|
|
69
|
+
name: false,
|
|
70
|
+
boxHeight: circle.r * 2,
|
|
71
|
+
boxWidth: circle.r * 2,
|
|
72
|
+
avatar: {
|
|
73
|
+
size: circle.r * 2
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
0.5,
|
|
77
|
+
config.imageFormat
|
|
78
|
+
));
|
|
79
|
+
}
|
|
80
|
+
composer.height = config.width;
|
|
81
|
+
return composer.generateSvg();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
function lerp(a, b, t) {
|
|
85
|
+
if (t < 0)
|
|
86
|
+
return a;
|
|
87
|
+
return a + (b - a) * t;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function tiersComposer(composer, sponsors, config) {
|
|
91
|
+
const tierPartitions = partitionTiers(sponsors, config.tiers, config.includePastSponsors);
|
|
92
|
+
composer.addSpan(config.padding?.top ?? 20);
|
|
93
|
+
for (const { tier: t, sponsors: sponsors2 } of tierPartitions) {
|
|
94
|
+
t.composeBefore?.(composer, sponsors2, config);
|
|
95
|
+
if (t.compose) {
|
|
96
|
+
t.compose(composer, sponsors2, config);
|
|
97
|
+
} else {
|
|
98
|
+
const preset = t.preset || tierPresets.base;
|
|
99
|
+
if (sponsors2.length && preset.avatar.size) {
|
|
100
|
+
const paddingTop = t.padding?.top ?? 20;
|
|
101
|
+
const paddingBottom = t.padding?.bottom ?? 10;
|
|
102
|
+
if (paddingTop)
|
|
103
|
+
composer.addSpan(paddingTop);
|
|
104
|
+
if (t.title) {
|
|
105
|
+
composer.addTitle(t.title).addSpan(5);
|
|
106
|
+
}
|
|
107
|
+
await composer.addSponsorGrid(sponsors2, preset);
|
|
108
|
+
if (paddingBottom)
|
|
109
|
+
composer.addSpan(paddingBottom);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
t.composeAfter?.(composer, sponsors2, config);
|
|
113
|
+
}
|
|
114
|
+
composer.addSpan(config.padding?.bottom ?? 20);
|
|
115
|
+
}
|
|
116
|
+
const tiersRenderer = {
|
|
117
|
+
name: "contribkit:tiers",
|
|
118
|
+
async renderSVG(config, sponsors) {
|
|
119
|
+
const composer = new SvgComposer(config);
|
|
120
|
+
await (config.customComposer || tiersComposer)(composer, sponsors, config);
|
|
121
|
+
return composer.generateSvg();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const builtinRenderers = {
|
|
126
|
+
tiers: tiersRenderer,
|
|
127
|
+
circles: circlesRenderer
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function r(path) {
|
|
131
|
+
return `./${relative(process.cwd(), path)}`;
|
|
132
|
+
}
|
|
133
|
+
async function run(inlineConfig, t = consola) {
|
|
134
|
+
t.log(`
|
|
135
|
+
${c.magenta.bold`ContribKit`} ${c.dim`v${version}`}
|
|
136
|
+
`);
|
|
137
|
+
const fullConfig = await loadConfig(inlineConfig);
|
|
138
|
+
const config = fullConfig;
|
|
139
|
+
const dir = resolve(process.cwd(), config.outputDir);
|
|
140
|
+
const cacheFile = resolve(dir, config.cacheFile);
|
|
141
|
+
const providers = resolveProviders(config.providers || guessProviders(config));
|
|
142
|
+
if (config.renders?.length) {
|
|
143
|
+
const names = /* @__PURE__ */ new Set();
|
|
144
|
+
config.renders.forEach((renderOptions, idx) => {
|
|
145
|
+
const name = renderOptions.name || "sponsors";
|
|
146
|
+
if (names.has(name))
|
|
147
|
+
throw new Error(`Duplicate render name: ${name} at index ${idx}`);
|
|
148
|
+
names.add(name);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const linksReplacements = normalizeReplacements(config.replaceLinks);
|
|
152
|
+
const avatarsReplacements = normalizeReplacements(config.replaceAvatars);
|
|
153
|
+
let allSponsors = [];
|
|
154
|
+
if (!fs.existsSync(cacheFile) || config.force) {
|
|
155
|
+
for (const i of providers) {
|
|
156
|
+
t.info(`Fetching sponsorships from ${i.name}...`);
|
|
157
|
+
let sponsors = await i.fetchSponsors(config);
|
|
158
|
+
sponsors.forEach((s) => s.provider = i.name);
|
|
159
|
+
sponsors = await config.onSponsorsFetched?.(sponsors, i.name) || sponsors;
|
|
160
|
+
t.success(`${sponsors.length} sponsorships fetched from ${i.name}`);
|
|
161
|
+
allSponsors.push(...sponsors);
|
|
162
|
+
}
|
|
163
|
+
allSponsors = await config.onSponsorsAllFetched?.(allSponsors) || allSponsors;
|
|
164
|
+
{
|
|
165
|
+
let pushGroup = function(group) {
|
|
166
|
+
const existingSets = new Set(group.map((s) => sponsorsMergeMap.get(s)).filter(notNullish));
|
|
167
|
+
let set;
|
|
168
|
+
if (existingSets.size === 1) {
|
|
169
|
+
set = [...existingSets.values()][0];
|
|
170
|
+
} else if (existingSets.size === 0) {
|
|
171
|
+
set = new Set(group);
|
|
172
|
+
} else {
|
|
173
|
+
set = /* @__PURE__ */ new Set();
|
|
174
|
+
for (const s of existingSets) {
|
|
175
|
+
for (const i of s)
|
|
176
|
+
set.add(i);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const s of group) {
|
|
180
|
+
set.add(s);
|
|
181
|
+
sponsorsMergeMap.set(s, set);
|
|
182
|
+
}
|
|
183
|
+
}, matchSponsor = function(sponsor, matcher) {
|
|
184
|
+
if (matcher.provider && sponsor.provider !== matcher.provider)
|
|
185
|
+
return false;
|
|
186
|
+
if (matcher.login && sponsor.sponsor.login !== matcher.login)
|
|
187
|
+
return false;
|
|
188
|
+
if (matcher.name && sponsor.sponsor.name !== matcher.name)
|
|
189
|
+
return false;
|
|
190
|
+
if (matcher.type && sponsor.sponsor.type !== matcher.type)
|
|
191
|
+
return false;
|
|
192
|
+
return true;
|
|
193
|
+
}, mergeSponsors = function(main, sponsors) {
|
|
194
|
+
const all = [main, ...sponsors];
|
|
195
|
+
main.isOneTime = all.every((s) => s.isOneTime);
|
|
196
|
+
main.expireAt = all.map((s) => s.expireAt).filter(notNullish).sort((a, b) => b.localeCompare(a))[0];
|
|
197
|
+
main.createdAt = all.map((s) => s.createdAt).filter(notNullish).sort((a, b) => a.localeCompare(b))[0];
|
|
198
|
+
main.monthlyDollars = all.every((s) => s.monthlyDollars === -1) ? -1 : all.filter((s) => s.monthlyDollars > 0).reduce((a, b) => a + b.monthlyDollars, 0);
|
|
199
|
+
main.provider = [...new Set(all.map((s) => s.provider))].join("+");
|
|
200
|
+
return main;
|
|
201
|
+
};
|
|
202
|
+
const sponsorsMergeMap = /* @__PURE__ */ new Map();
|
|
203
|
+
for (const rule of config.mergeSponsors || []) {
|
|
204
|
+
if (typeof rule === "function") {
|
|
205
|
+
for (const ship of allSponsors) {
|
|
206
|
+
const result = rule(ship, allSponsors);
|
|
207
|
+
if (result)
|
|
208
|
+
pushGroup(result);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
const group = rule.flatMap((matcher) => {
|
|
212
|
+
const matched = allSponsors.filter((s) => matchSponsor(s, matcher));
|
|
213
|
+
if (!matched.length)
|
|
214
|
+
t.warn(`No sponsor matched for ${JSON.stringify(matcher)}`);
|
|
215
|
+
return matched;
|
|
216
|
+
});
|
|
217
|
+
pushGroup(group);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (config.sponsorsAutoMerge) {
|
|
221
|
+
for (const ship of allSponsors) {
|
|
222
|
+
if (!ship.sponsor.socialLogins)
|
|
223
|
+
continue;
|
|
224
|
+
for (const [provider, login] of Object.entries(ship.sponsor.socialLogins)) {
|
|
225
|
+
const matched = allSponsors.filter((s) => s.sponsor.login === login && s.provider === provider);
|
|
226
|
+
if (matched)
|
|
227
|
+
pushGroup([ship, ...matched]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const removeSponsors = /* @__PURE__ */ new Set();
|
|
232
|
+
const groups = new Set(sponsorsMergeMap.values());
|
|
233
|
+
for (const group of groups) {
|
|
234
|
+
if (group.size === 1)
|
|
235
|
+
continue;
|
|
236
|
+
const sorted = [...group].sort((a, b) => allSponsors.indexOf(a) - allSponsors.indexOf(b));
|
|
237
|
+
t.info(`Merging ${sorted.map((i) => c.cyan`@${i.sponsor.login}(${i.provider})`).join(" + ")}`);
|
|
238
|
+
for (const s of sorted.slice(1))
|
|
239
|
+
removeSponsors.add(s);
|
|
240
|
+
mergeSponsors(sorted[0], sorted.slice(1));
|
|
241
|
+
}
|
|
242
|
+
allSponsors = allSponsors.filter((s) => !removeSponsors.has(s));
|
|
243
|
+
}
|
|
244
|
+
allSponsors.forEach((ship) => {
|
|
245
|
+
for (const r2 of linksReplacements) {
|
|
246
|
+
if (typeof r2 === "function") {
|
|
247
|
+
const result = r2(ship);
|
|
248
|
+
if (result) {
|
|
249
|
+
ship.sponsor.linkUrl = result;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
} else if (r2[0] === ship.sponsor.linkUrl) {
|
|
253
|
+
ship.sponsor.linkUrl = r2[1];
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const r2 of avatarsReplacements) {
|
|
258
|
+
if (typeof r2 === "function") {
|
|
259
|
+
const result = r2(ship);
|
|
260
|
+
if (result) {
|
|
261
|
+
ship.sponsor.avatarUrl = result;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
} else if (r2[0] === ship.sponsor.avatarUrl) {
|
|
265
|
+
ship.sponsor.avatarUrl = r2[1];
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
t.info("Resolving avatars...");
|
|
271
|
+
await resolveAvatars(allSponsors, config.fallbackAvatar, t);
|
|
272
|
+
t.success("Avatars resolved");
|
|
273
|
+
await fsp.mkdir(dirname(cacheFile), { recursive: true });
|
|
274
|
+
await fsp.writeFile(cacheFile, stringifyCache(allSponsors));
|
|
275
|
+
} else {
|
|
276
|
+
allSponsors = parseCache(await fsp.readFile(cacheFile, "utf8"));
|
|
277
|
+
t.success(`Loaded from cache ${r(cacheFile)}`);
|
|
278
|
+
}
|
|
279
|
+
allSponsors.sort(
|
|
280
|
+
(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)
|
|
281
|
+
// ASC name
|
|
282
|
+
);
|
|
283
|
+
allSponsors = await config.onSponsorsReady?.(allSponsors) || allSponsors;
|
|
284
|
+
if (config.renders?.length) {
|
|
285
|
+
t.info(`Generating with ${config.renders.length} renders...`);
|
|
286
|
+
await Promise.all(config.renders.map(async (renderOptions) => {
|
|
287
|
+
const mergedOptions = {
|
|
288
|
+
...fullConfig,
|
|
289
|
+
...renderOptions
|
|
290
|
+
};
|
|
291
|
+
const renderer = builtinRenderers[mergedOptions.renderer || "tiers"];
|
|
292
|
+
await applyRenderer(
|
|
293
|
+
renderer,
|
|
294
|
+
config,
|
|
295
|
+
mergedOptions,
|
|
296
|
+
allSponsors,
|
|
297
|
+
t
|
|
298
|
+
);
|
|
299
|
+
}));
|
|
300
|
+
} else {
|
|
301
|
+
const renderer = builtinRenderers[fullConfig.renderer || "tiers"];
|
|
302
|
+
await applyRenderer(
|
|
303
|
+
renderer,
|
|
304
|
+
config,
|
|
305
|
+
fullConfig,
|
|
306
|
+
allSponsors,
|
|
307
|
+
t
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function applyRenderer(renderer, config, renderOptions, sponsors, t = consola) {
|
|
312
|
+
sponsors = [...sponsors];
|
|
313
|
+
sponsors = await renderOptions.onBeforeRenderer?.(sponsors) || sponsors;
|
|
314
|
+
const logPrefix = c.dim`[${renderOptions.name}]`;
|
|
315
|
+
const dir = resolve(process.cwd(), config.outputDir);
|
|
316
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
317
|
+
if (renderOptions.filter)
|
|
318
|
+
sponsors = sponsors.filter((s) => renderOptions.filter(s, sponsors) !== false);
|
|
319
|
+
if (!renderOptions.includePrivate)
|
|
320
|
+
sponsors = sponsors.filter((s) => s.privacyLevel !== "PRIVATE");
|
|
321
|
+
if (!renderOptions.imageFormat)
|
|
322
|
+
renderOptions.imageFormat = "webp";
|
|
323
|
+
const processingSvg = (async () => {
|
|
324
|
+
let svgWebp = await renderer.renderSVG(renderOptions, sponsors);
|
|
325
|
+
if (renderOptions.onSvgGenerated) {
|
|
326
|
+
svgWebp = await renderOptions.onSvgGenerated(svgWebp) || svgWebp;
|
|
327
|
+
}
|
|
328
|
+
return svgWebp;
|
|
329
|
+
})();
|
|
330
|
+
if (renderOptions.formats) {
|
|
331
|
+
let svgPng;
|
|
332
|
+
await Promise.all([
|
|
333
|
+
renderOptions.formats.map(async (format) => {
|
|
334
|
+
if (!outputFormats.includes(format))
|
|
335
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
336
|
+
const path = join(dir, `${renderOptions.name}.${format}`);
|
|
337
|
+
let data;
|
|
338
|
+
if (format === "svg") {
|
|
339
|
+
t.info(`${logPrefix} Composing SVG...`);
|
|
340
|
+
data = await processingSvg;
|
|
341
|
+
}
|
|
342
|
+
if (format === "json") {
|
|
343
|
+
data = JSON.stringify(sponsors, null, 2);
|
|
344
|
+
}
|
|
345
|
+
if (format === "png" || format === "webp") {
|
|
346
|
+
if (!svgPng) {
|
|
347
|
+
svgPng = renderer.renderSVG({
|
|
348
|
+
...renderOptions,
|
|
349
|
+
imageFormat: "png"
|
|
350
|
+
}, sponsors);
|
|
351
|
+
}
|
|
352
|
+
if (format === "png") {
|
|
353
|
+
data = await svgToPng(await svgPng);
|
|
354
|
+
}
|
|
355
|
+
if (format === "webp") {
|
|
356
|
+
data = await svgToWebp(await svgPng);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
await fsp.writeFile(path, data);
|
|
360
|
+
t.success(`${logPrefix} Wrote to ${r(path)}`);
|
|
361
|
+
})
|
|
362
|
+
]);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function normalizeReplacements(replaces) {
|
|
366
|
+
const array = (Array.isArray(replaces) ? replaces : [replaces]).filter(notNullish);
|
|
367
|
+
const entries = array.map((i) => {
|
|
368
|
+
if (!i)
|
|
369
|
+
return [];
|
|
370
|
+
if (typeof i === "function")
|
|
371
|
+
return [i];
|
|
372
|
+
return Object.entries(i);
|
|
373
|
+
}).flat();
|
|
374
|
+
return entries;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const cli = cac("contributors-svg").version(version).help();
|
|
378
|
+
cli.command("[outputDir]", "Generate contributors SVG").option("--width, -w <width>", "SVG width", { default: 800 }).option("--fallback-avatar, --fallback <url>", "Fallback avatar URL").option("--force, -f", "Force regeneration", { default: false }).option("--name <name>", "Name").option("--filter <filter>", "Filter contributors").option("--output-dir, -o, --dir <dir>", "Output directory").action(async (outputDir, options) => {
|
|
379
|
+
const config = options;
|
|
380
|
+
if (outputDir)
|
|
381
|
+
config.outputDir = outputDir;
|
|
382
|
+
if (options.filter)
|
|
383
|
+
config.filter = createFilterFromString(options.filter);
|
|
384
|
+
await run(config);
|
|
385
|
+
});
|
|
386
|
+
cli.parse();
|
|
387
|
+
function createFilterFromString(template) {
|
|
388
|
+
const [_, op, value] = template.split(/([<>=]+)/);
|
|
389
|
+
const num = Number.parseInt(value);
|
|
390
|
+
if (op === "<")
|
|
391
|
+
return (s) => s.monthlyDollars < num;
|
|
392
|
+
if (op === "<=")
|
|
393
|
+
return (s) => s.monthlyDollars <= num;
|
|
394
|
+
if (op === ">")
|
|
395
|
+
return (s) => s.monthlyDollars > num;
|
|
396
|
+
if (op === ">=")
|
|
397
|
+
return (s) => s.monthlyDollars >= num;
|
|
398
|
+
throw new Error(`Unable to parse filter template ${template}`);
|
|
399
|
+
}
|