@lizardbyte/contribkit 2025.821.193159 → 2025.1130.1103

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # ContribKit
2
2
 
3
3
  [![GitHub stars](https://img.shields.io/github/stars/lizardbyte/contribkit.svg?logo=github&style=for-the-badge)](https://github.com/LizardByte/contribkit)
4
- [![GitHub Workflow Status (CI)](https://img.shields.io/github/actions/workflow/status/lizardbyte/contribkit/ci.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/contribkit/actions/workflows/CI.yml?query=branch%3Amaster)
4
+ [![GitHub Workflow Status (CI)](https://img.shields.io/github/actions/workflow/status/lizardbyte/contribkit/_ci-node.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/contribkit/actions/workflows/CI.yml?query=branch%3Amaster)
5
5
  [![Codecov](https://img.shields.io/codecov/c/gh/LizardByte/contribkit?token=WBivqQDwFw&style=for-the-badge&logo=codecov&label=codecov)](https://codecov.io/gh/LizardByte/contribkit)
6
6
  [![NPM Monthly Downloads](https://img.shields.io/npm/dm/%40lizardbyte%2Fcontribkit?style=for-the-badge&logo=npm&label=npm%20downloads/m)](https://www.npmjs.com/package/@lizardbyte/contribkit)
7
7
  [![NPM Version](https://img.shields.io/npm/v/%40lizardbyte%2Fcontribkit?style=for-the-badge&logo=npm&label=npm%20version)](https://www.npmjs.com/package/@lizardbyte/contribkit)
@@ -13,7 +13,8 @@ Supports:
13
13
 
14
14
  - Contributors:
15
15
  - [**CrowdIn**](https://crowdin.com)
16
- - [**GitHub**](https://github.com)
16
+ - [**GitHub Contributors**](https://github.com) (contributors to a specific repository)
17
+ - [**GitHub Contributions**](https://github.com) (merged PRs aggregated by repository owner across all repos for a single user)
17
18
  - [**Gitlab**](https://gitlab.com)
18
19
  - Sponsors:
19
20
  - [**GitHub Sponsors**](https://github.com/sponsors)
@@ -37,11 +38,25 @@ CONTRIBKIT_CROWDIN_MIN_TRANSLATIONS=1
37
38
 
38
39
  ; GitHubContributors provider.
39
40
  ; Token requires the `public_repo` and `read:user` scopes.
41
+ ; This provider tracks all contributors to a specific repository.
40
42
  CONTRIBKIT_GITHUB_CONTRIBUTORS_TOKEN=
41
43
  CONTRIBKIT_GITHUB_CONTRIBUTORS_LOGIN=
42
44
  CONTRIBKIT_GITHUB_CONTRIBUTORS_MIN=1
43
45
  CONTRIBKIT_GITHUB_CONTRIBUTORS_REPO=
44
46
 
47
+ ; GitHubContributions provider.
48
+ ; Token requires the `read:user` scope.
49
+ ; This provider aggregates merged pull requests across all repositories by repository owner (user or organization).
50
+ ; Each owner appears once with the total merged PRs you authored to their repos.
51
+ ; Avatar and link point to the owner (or to the repo if only one repo per owner).
52
+ ; Only merged PRs are counted - open or closed-without-merge PRs are excluded.
53
+ CONTRIBKIT_GITHUB_CONTRIBUTIONS_TOKEN=
54
+ CONTRIBKIT_GITHUB_CONTRIBUTIONS_LOGIN=
55
+ ; Optional: Cap the maximum contribution count per org/user (useful for circles visualization)
56
+ CONTRIBKIT_GITHUB_CONTRIBUTIONS_MAX=
57
+ ; Optional: Apply logarithmic scaling to reduce dominance of high contributors (true/false)
58
+ CONTRIBKIT_GITHUB_CONTRIBUTIONS_LOGARITHMIC=
59
+
45
60
  ; GitlabContributors provider.
46
61
  ; Token requires the `read_api` and `read_user` scopes.
47
62
  CONTRIBKIT_GITLAB_CONTRIBUTORS_TOKEN=
@@ -96,9 +111,26 @@ CONTRIBKIT_LIBERAPAY_LOGIN=
96
111
  > This will require different env variables to be set for each provider, and to be created from separate
97
112
  > commands.
98
113
 
114
+ #### GitHub Provider Options
115
+
116
+ There are two GitHub contributor providers available:
117
+
118
+ - **GitHubContributors**: Tracks all contributors to a specific repository (e.g., `owner/repo`). Each contributor appears once with their actual contribution count to that repository.
119
+ - **GitHubContributions**: Aggregates a single user's **merged pull requests** across all repositories, grouped by repository owner (user or organization). Each owner appears once with the total merged PRs. The avatar and link point to the owner (or to the specific repo if only one repo per owner).
120
+
121
+ Use **GitHubContributors** when you want to showcase everyone who has contributed to your project with their contribution counts.
122
+ Use **GitHubContributions** when you want to understand where a single user's completed contributions (merged PRs) have gone, without overwhelming duplicates per repo under the same owner.
123
+
124
+ **GitHubContributions accuracy**:
125
+ - Counts only **merged** pull requests - open or closed-without-merge PRs are excluded
126
+ - Discovers repos via **2 sources**:
127
+ 1. **contributionsCollection** - Yearly commit timeline (full history) for discovering repositories you have committed to
128
+ 2. **Search API** - Repositories where you have merged PRs (`is:pr is:merged author:login`)
129
+ - When an owner has only one repo, the link points to that repo; otherwise to the owner profile
130
+
99
131
  Run:
100
132
 
101
- ```base
133
+ ```bash
102
134
  npx contribkit
103
135
  ```
104
136
 
@@ -133,6 +165,11 @@ export default defineConfig({
133
165
  // ...
134
166
  },
135
167
 
168
+ // For contributor providers:
169
+ githubContributions: {
170
+ login: 'username',
171
+ },
172
+
136
173
  // Rendering configs
137
174
  width: 800,
138
175
  renderer: 'tiers', // or 'circles'
@@ -43,6 +43,12 @@ class Queue {
43
43
 
44
44
  this.#head = this.#head.next;
45
45
  this.#size--;
46
+
47
+ // Clean up tail reference when queue becomes empty
48
+ if (!this.#head) {
49
+ this.#tail = undefined;
50
+ }
51
+
46
52
  return current.value;
47
53
  }
48
54
 
@@ -1514,7 +1514,7 @@ var hasRequiredLodash_groupby;
1514
1514
  function requireLodash_groupby () {
1515
1515
  if (hasRequiredLodash_groupby) return lodash_groupby.exports;
1516
1516
  hasRequiredLodash_groupby = 1;
1517
- (function (module, exports) {
1517
+ (function (module, exports$1) {
1518
1518
  /** Used as the size to enable large array optimizations. */
1519
1519
  var LARGE_ARRAY_SIZE = 200;
1520
1520
 
@@ -1608,7 +1608,7 @@ function requireLodash_groupby () {
1608
1608
  var root = freeGlobal || freeSelf || Function('return this')();
1609
1609
 
1610
1610
  /** Detect free variable `exports`. */
1611
- var freeExports = exports && !exports.nodeType && exports;
1611
+ var freeExports = exports$1 && !exports$1.nodeType && exports$1;
1612
1612
 
1613
1613
  /** Detect free variable `module`. */
1614
1614
  var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module;
@@ -4007,13 +4007,13 @@ var hasRequiredTransforms;
4007
4007
  function requireTransforms () {
4008
4008
  if (hasRequiredTransforms) return transforms;
4009
4009
  hasRequiredTransforms = 1;
4010
- (function (exports) {
4011
- Object.defineProperty(exports, "__esModule", { value: true });
4012
- exports.HeaderTransformer = exports.RowTransformerValidator = void 0;
4010
+ (function (exports$1) {
4011
+ Object.defineProperty(exports$1, "__esModule", { value: true });
4012
+ exports$1.HeaderTransformer = exports$1.RowTransformerValidator = void 0;
4013
4013
  var RowTransformerValidator_1 = requireRowTransformerValidator();
4014
- Object.defineProperty(exports, "RowTransformerValidator", { enumerable: true, get: function () { return RowTransformerValidator_1.RowTransformerValidator; } });
4014
+ Object.defineProperty(exports$1, "RowTransformerValidator", { enumerable: true, get: function () { return RowTransformerValidator_1.RowTransformerValidator; } });
4015
4015
  var HeaderTransformer_1 = requireHeaderTransformer();
4016
- Object.defineProperty(exports, "HeaderTransformer", { enumerable: true, get: function () { return HeaderTransformer_1.HeaderTransformer; } });
4016
+ Object.defineProperty(exports$1, "HeaderTransformer", { enumerable: true, get: function () { return HeaderTransformer_1.HeaderTransformer; } });
4017
4017
 
4018
4018
  } (transforms));
4019
4019
  return transforms;
@@ -4392,17 +4392,17 @@ var hasRequiredColumn;
4392
4392
  function requireColumn () {
4393
4393
  if (hasRequiredColumn) return column;
4394
4394
  hasRequiredColumn = 1;
4395
- (function (exports) {
4396
- Object.defineProperty(exports, "__esModule", { value: true });
4397
- exports.ColumnFormatter = exports.QuotedColumnParser = exports.NonQuotedColumnParser = exports.ColumnParser = void 0;
4395
+ (function (exports$1) {
4396
+ Object.defineProperty(exports$1, "__esModule", { value: true });
4397
+ exports$1.ColumnFormatter = exports$1.QuotedColumnParser = exports$1.NonQuotedColumnParser = exports$1.ColumnParser = void 0;
4398
4398
  var ColumnParser_1 = requireColumnParser();
4399
- Object.defineProperty(exports, "ColumnParser", { enumerable: true, get: function () { return ColumnParser_1.ColumnParser; } });
4399
+ Object.defineProperty(exports$1, "ColumnParser", { enumerable: true, get: function () { return ColumnParser_1.ColumnParser; } });
4400
4400
  var NonQuotedColumnParser_1 = requireNonQuotedColumnParser();
4401
- Object.defineProperty(exports, "NonQuotedColumnParser", { enumerable: true, get: function () { return NonQuotedColumnParser_1.NonQuotedColumnParser; } });
4401
+ Object.defineProperty(exports$1, "NonQuotedColumnParser", { enumerable: true, get: function () { return NonQuotedColumnParser_1.NonQuotedColumnParser; } });
4402
4402
  var QuotedColumnParser_1 = requireQuotedColumnParser();
4403
- Object.defineProperty(exports, "QuotedColumnParser", { enumerable: true, get: function () { return QuotedColumnParser_1.QuotedColumnParser; } });
4403
+ Object.defineProperty(exports$1, "QuotedColumnParser", { enumerable: true, get: function () { return QuotedColumnParser_1.QuotedColumnParser; } });
4404
4404
  var ColumnFormatter_1 = requireColumnFormatter();
4405
- Object.defineProperty(exports, "ColumnFormatter", { enumerable: true, get: function () { return ColumnFormatter_1.ColumnFormatter; } });
4405
+ Object.defineProperty(exports$1, "ColumnFormatter", { enumerable: true, get: function () { return ColumnFormatter_1.ColumnFormatter; } });
4406
4406
 
4407
4407
  } (column));
4408
4408
  return column;
@@ -4583,21 +4583,21 @@ var hasRequiredParser;
4583
4583
  function requireParser () {
4584
4584
  if (hasRequiredParser) return parser;
4585
4585
  hasRequiredParser = 1;
4586
- (function (exports) {
4587
- Object.defineProperty(exports, "__esModule", { value: true });
4588
- exports.QuotedColumnParser = exports.NonQuotedColumnParser = exports.ColumnParser = exports.Token = exports.Scanner = exports.RowParser = exports.Parser = void 0;
4586
+ (function (exports$1) {
4587
+ Object.defineProperty(exports$1, "__esModule", { value: true });
4588
+ exports$1.QuotedColumnParser = exports$1.NonQuotedColumnParser = exports$1.ColumnParser = exports$1.Token = exports$1.Scanner = exports$1.RowParser = exports$1.Parser = void 0;
4589
4589
  var Parser_1 = requireParser$1();
4590
- Object.defineProperty(exports, "Parser", { enumerable: true, get: function () { return Parser_1.Parser; } });
4590
+ Object.defineProperty(exports$1, "Parser", { enumerable: true, get: function () { return Parser_1.Parser; } });
4591
4591
  var RowParser_1 = requireRowParser();
4592
- Object.defineProperty(exports, "RowParser", { enumerable: true, get: function () { return RowParser_1.RowParser; } });
4592
+ Object.defineProperty(exports$1, "RowParser", { enumerable: true, get: function () { return RowParser_1.RowParser; } });
4593
4593
  var Scanner_1 = requireScanner();
4594
- Object.defineProperty(exports, "Scanner", { enumerable: true, get: function () { return Scanner_1.Scanner; } });
4594
+ Object.defineProperty(exports$1, "Scanner", { enumerable: true, get: function () { return Scanner_1.Scanner; } });
4595
4595
  var Token_1 = requireToken();
4596
- Object.defineProperty(exports, "Token", { enumerable: true, get: function () { return Token_1.Token; } });
4596
+ Object.defineProperty(exports$1, "Token", { enumerable: true, get: function () { return Token_1.Token; } });
4597
4597
  var column_1 = requireColumn();
4598
- Object.defineProperty(exports, "ColumnParser", { enumerable: true, get: function () { return column_1.ColumnParser; } });
4599
- Object.defineProperty(exports, "NonQuotedColumnParser", { enumerable: true, get: function () { return column_1.NonQuotedColumnParser; } });
4600
- Object.defineProperty(exports, "QuotedColumnParser", { enumerable: true, get: function () { return column_1.QuotedColumnParser; } });
4598
+ Object.defineProperty(exports$1, "ColumnParser", { enumerable: true, get: function () { return column_1.ColumnParser; } });
4599
+ Object.defineProperty(exports$1, "NonQuotedColumnParser", { enumerable: true, get: function () { return column_1.NonQuotedColumnParser; } });
4600
+ Object.defineProperty(exports$1, "QuotedColumnParser", { enumerable: true, get: function () { return column_1.QuotedColumnParser; } });
4601
4601
 
4602
4602
  } (parser));
4603
4603
  return parser;
@@ -4834,7 +4834,7 @@ var hasRequiredSrc;
4834
4834
  function requireSrc () {
4835
4835
  if (hasRequiredSrc) return src;
4836
4836
  hasRequiredSrc = 1;
4837
- (function (exports) {
4837
+ (function (exports$1) {
4838
4838
  var __createBinding = (src && src.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4839
4839
  if (k2 === undefined) k2 = k;
4840
4840
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -4858,39 +4858,39 @@ function requireSrc () {
4858
4858
  __setModuleDefault(result, mod);
4859
4859
  return result;
4860
4860
  };
4861
- var __exportStar = (src && src.__exportStar) || function(m, exports) {
4862
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
4861
+ var __exportStar = (src && src.__exportStar) || function(m, exports$1) {
4862
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports$1, p)) __createBinding(exports$1, m, p);
4863
4863
  };
4864
- Object.defineProperty(exports, "__esModule", { value: true });
4865
- exports.parseString = exports.parseFile = exports.parseStream = exports.parse = exports.ParserOptions = exports.CsvParserStream = void 0;
4864
+ Object.defineProperty(exports$1, "__esModule", { value: true });
4865
+ exports$1.parseString = exports$1.parseFile = exports$1.parseStream = exports$1.parse = exports$1.ParserOptions = exports$1.CsvParserStream = void 0;
4866
4866
  const fs = __importStar(require$$0$1);
4867
4867
  const stream_1 = require$$1;
4868
4868
  const ParserOptions_1 = requireParserOptions();
4869
4869
  const CsvParserStream_1 = requireCsvParserStream();
4870
- __exportStar(requireTypes(), exports);
4870
+ __exportStar(requireTypes(), exports$1);
4871
4871
  var CsvParserStream_2 = requireCsvParserStream();
4872
- Object.defineProperty(exports, "CsvParserStream", { enumerable: true, get: function () { return CsvParserStream_2.CsvParserStream; } });
4872
+ Object.defineProperty(exports$1, "CsvParserStream", { enumerable: true, get: function () { return CsvParserStream_2.CsvParserStream; } });
4873
4873
  var ParserOptions_2 = requireParserOptions();
4874
- Object.defineProperty(exports, "ParserOptions", { enumerable: true, get: function () { return ParserOptions_2.ParserOptions; } });
4874
+ Object.defineProperty(exports$1, "ParserOptions", { enumerable: true, get: function () { return ParserOptions_2.ParserOptions; } });
4875
4875
  const parse = (args) => {
4876
4876
  return new CsvParserStream_1.CsvParserStream(new ParserOptions_1.ParserOptions(args));
4877
4877
  };
4878
- exports.parse = parse;
4878
+ exports$1.parse = parse;
4879
4879
  const parseStream = (stream, options) => {
4880
4880
  return stream.pipe(new CsvParserStream_1.CsvParserStream(new ParserOptions_1.ParserOptions(options)));
4881
4881
  };
4882
- exports.parseStream = parseStream;
4882
+ exports$1.parseStream = parseStream;
4883
4883
  const parseFile = (location, options = {}) => {
4884
4884
  return fs.createReadStream(location).pipe(new CsvParserStream_1.CsvParserStream(new ParserOptions_1.ParserOptions(options)));
4885
4885
  };
4886
- exports.parseFile = parseFile;
4886
+ exports$1.parseFile = parseFile;
4887
4887
  const parseString = (string, options) => {
4888
4888
  const rs = new stream_1.Readable();
4889
4889
  rs.push(string);
4890
4890
  rs.push(null);
4891
4891
  return rs.pipe(new CsvParserStream_1.CsvParserStream(new ParserOptions_1.ParserOptions(options)));
4892
4892
  };
4893
- exports.parseString = parseString;
4893
+ exports$1.parseString = parseString;
4894
4894
 
4895
4895
  } (src));
4896
4896
  return src;
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
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.C8dVxxYO.mjs';
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.DLdCPKip.mjs';
3
3
  import fs from 'node:fs';
4
4
  import fsp from 'node:fs/promises';
5
5
  import { resolve, dirname, relative, join } from 'node:path';
package/dist/index.d.mts CHANGED
@@ -83,7 +83,7 @@ interface Sponsorship {
83
83
  }
84
84
  declare const outputFormats: readonly ["svg", "png", "webp", "json"];
85
85
  type OutputFormat = typeof outputFormats[number];
86
- type ProviderName = 'github' | 'patreon' | 'opencollective' | 'afdian' | 'polar' | 'liberapay' | 'githubContributors' | 'gitlabContributors' | 'crowdinContributors';
86
+ type ProviderName = 'github' | 'patreon' | 'opencollective' | 'afdian' | 'polar' | 'liberapay' | 'githubContributors' | 'gitlabContributors' | 'crowdinContributors' | 'githubContributions';
87
87
  type GitHubAccountType = 'user' | 'organization';
88
88
  interface ProvidersConfig {
89
89
  github?: {
@@ -278,6 +278,36 @@ interface ProvidersConfig {
278
278
  */
279
279
  minTranslations?: number;
280
280
  };
281
+ githubContributions?: {
282
+ /**
283
+ * GitHub user login to fetch contributions for.
284
+ *
285
+ * Will read from `CONTRIBKIT_GITHUB_CONTRIBUTIONS_LOGIN` environment variable if not set.
286
+ */
287
+ login?: string;
288
+ /**
289
+ * GitHub Token that has access to read user contributions.
290
+ *
291
+ * Will read from `CONTRIBKIT_GITHUB_CONTRIBUTIONS_TOKEN` environment variable if not set.
292
+ *
293
+ * @deprecated It's not recommended set this value directly, pass from env or use `.env` file.
294
+ */
295
+ token?: string;
296
+ /**
297
+ * Cap the maximum contribution count per organization/user.
298
+ * Useful to prevent one dominant contributor from overshadowing others in visualizations.
299
+ *
300
+ * @example 100 // Cap all contributions at 100 PRs max
301
+ */
302
+ maxContributions?: number;
303
+ /**
304
+ * Apply logarithmic scaling to contribution counts.
305
+ * Useful to reduce the visual dominance of high contributors while maintaining relative differences.
306
+ *
307
+ * @default false
308
+ */
309
+ logarithmicScaling?: boolean;
310
+ };
281
311
  }
282
312
  interface ContribkitRenderOptions {
283
313
  /**
@@ -557,6 +587,7 @@ declare const ProvidersMap: {
557
587
  polar: Provider;
558
588
  liberapay: Provider;
559
589
  githubContributors: Provider;
590
+ githubContributions: Provider;
560
591
  gitlabContributors: Provider;
561
592
  crowdinContributors: Provider;
562
593
  };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { F as FALLBACK_AVATAR, G as GitHubProvider, P as ProvidersMap, S as SvgComposer, c as defaultConfig, b as defaultInlineCSS, a as defaultTiers, d as defineConfig, n as fetchGitHubSponsors, m as fetchSponsors, h as genSvgImage, i as generateBadge, j as guessProviders, l as loadConfig, o as makeQuery, q as outputFormats, p as partitionTiers, e as presets, f as resizeImage, r as resolveAvatars, k as resolveProviders, s as svgToPng, g as svgToWebp, t as tierPresets } from './shared/contribkit.C8dVxxYO.mjs';
1
+ export { F as FALLBACK_AVATAR, G as GitHubProvider, P as ProvidersMap, S as SvgComposer, c as defaultConfig, b as defaultInlineCSS, a as defaultTiers, d as defineConfig, n as fetchGitHubSponsors, m as fetchSponsors, h as genSvgImage, i as generateBadge, j as guessProviders, l as loadConfig, o as makeQuery, q as outputFormats, p as partitionTiers, e as presets, f as resizeImage, r as resolveAvatars, k as resolveProviders, s as svgToPng, g as svgToWebp, t as tierPresets } from './shared/contribkit.DLdCPKip.mjs';
2
2
  import 'unconfig';
3
3
  import 'node:process';
4
4
  import 'dotenv';
@@ -196,12 +196,18 @@ function loadEnv() {
196
196
  token: process.env.CONTRIBKIT_CROWDIN_TOKEN,
197
197
  projectId: Number(process.env.CONTRIBKIT_CROWDIN_PROJECT_ID),
198
198
  minTranslations: Number(process.env.CONTRIBKIT_CROWDIN_MIN_TRANSLATIONS) || 1
199
+ },
200
+ githubContributions: {
201
+ login: process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_LOGIN,
202
+ token: process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_TOKEN,
203
+ maxContributions: Number(process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_MAX) || void 0,
204
+ logarithmicScaling: process.env.CONTRIBKIT_GITHUB_CONTRIBUTIONS_LOGARITHMIC === "true"
199
205
  }
200
206
  };
201
207
  return JSON.parse(JSON.stringify(config));
202
208
  }
203
209
 
204
- const version = "2025.821.193159";
210
+ const version = "2025.1130.1103";
205
211
 
206
212
  async function fetchImage(url) {
207
213
  const arrayBuffer = await $fetch(url, {
@@ -345,7 +351,8 @@ function partitionTiers(sponsors, tiers, includePastSponsors) {
345
351
  }
346
352
 
347
353
  function genSvgImage(x, y, size, radius, base64Image, imageFormat) {
348
- const cropId = `c${crypto.createHash("md5").update(base64Image).digest("hex").slice(0, 6)}`;
354
+ const hashInput = `${x}:${y}:${size}:${radius}:${base64Image}`;
355
+ const cropId = `c${crypto.createHash("sha256").update(hashInput).digest("hex").slice(0, 6)}`;
349
356
  return `
350
357
  <clipPath id="${cropId}">
351
358
  <rect x="${x}" y="${y}" width="${size}" height="${size}" rx="${size * radius}" ry="${size * radius}" />
@@ -675,6 +682,8 @@ function normalizeUrl$1(urlString, options) {
675
682
  removeDirectoryIndex: false,
676
683
  removeExplicitPort: false,
677
684
  sortQueryParameters: true,
685
+ removePath: false,
686
+ transformPath: false,
678
687
  ...options,
679
688
  };
680
689
 
@@ -786,6 +795,18 @@ function normalizeUrl$1(urlString, options) {
786
795
  }
787
796
  }
788
797
 
798
+ // Remove path
799
+ if (options.removePath) {
800
+ urlObject.pathname = '/';
801
+ }
802
+
803
+ // Transform path components
804
+ if (options.transformPath && typeof options.transformPath === 'function') {
805
+ const pathComponents = urlObject.pathname.split('/').filter(Boolean);
806
+ const newComponents = options.transformPath(pathComponents);
807
+ urlObject.pathname = newComponents?.length > 0 ? `/${newComponents.join('/')}` : '/';
808
+ }
809
+
789
810
  if (urlObject.hostname) {
790
811
  // Remove trailing dot
791
812
  urlObject.hostname = urlObject.hostname.replace(/\.$/, '');
@@ -826,12 +847,21 @@ function normalizeUrl$1(urlString, options) {
826
847
 
827
848
  // Sort query parameters
828
849
  if (options.sortQueryParameters) {
850
+ const originalSearch = urlObject.search;
829
851
  urlObject.searchParams.sort();
830
852
 
831
853
  // Calling `.sort()` encodes the search parameters, so we need to decode them again.
832
854
  try {
833
855
  urlObject.search = decodeURIComponent(urlObject.search);
834
856
  } catch {}
857
+
858
+ // Fix parameters that originally had no equals sign but got one added by URLSearchParams
859
+ const partsWithoutEquals = originalSearch.slice(1).split('&').filter(p => p && !p.includes('='));
860
+ for (const part of partsWithoutEquals) {
861
+ const decoded = decodeURIComponent(part);
862
+ // Only replace at word boundaries to avoid partial matches
863
+ urlObject.search = urlObject.search.replace(`?${decoded}=`, `?${decoded}`).replace(`&${decoded}=`, `&${decoded}`);
864
+ }
835
865
  }
836
866
 
837
867
  if (options.removeTrailingSlash) {
@@ -1082,6 +1112,246 @@ async function fetchGitHubContributors(token, login, repo, minContributions = 1)
1082
1112
  }));
1083
1113
  }
1084
1114
 
1115
+ const GitHubContributionsProvider = {
1116
+ name: "githubContributions",
1117
+ fetchSponsors(config) {
1118
+ if (!config.githubContributions?.login)
1119
+ throw new Error("GitHub login is required for githubContributions provider");
1120
+ return fetchGitHubContributions(
1121
+ config.githubContributions?.token || config.token,
1122
+ config.githubContributions.login,
1123
+ config.githubContributions.maxContributions,
1124
+ config.githubContributions.logarithmicScaling
1125
+ );
1126
+ }
1127
+ };
1128
+ function createGraphQLFetch(token) {
1129
+ return async (body) => {
1130
+ return await $fetch("https://api.github.com/graphql", {
1131
+ method: "POST",
1132
+ headers: {
1133
+ Authorization: `bearer ${token}`,
1134
+ "Content-Type": "application/json"
1135
+ },
1136
+ body
1137
+ });
1138
+ };
1139
+ }
1140
+ async function fetchUserCreationDate(graphqlFetch, login) {
1141
+ const userInfoQuery = `
1142
+ query($login: String!) {
1143
+ user(login: $login) {
1144
+ createdAt
1145
+ }
1146
+ }
1147
+ `;
1148
+ const userInfo = await graphqlFetch({
1149
+ query: userInfoQuery,
1150
+ variables: { login }
1151
+ });
1152
+ return new Date(userInfo.data.user.createdAt);
1153
+ }
1154
+ function generateYearRanges(accountCreated, now) {
1155
+ const years = [];
1156
+ for (let year = accountCreated.getFullYear(); year <= now.getFullYear(); year++) {
1157
+ const from = year === accountCreated.getFullYear() ? accountCreated.toISOString() : `${year}-01-01T00:00:00Z`;
1158
+ const to = year === now.getFullYear() ? now.toISOString() : `${year}-12-31T23:59:59Z`;
1159
+ years.push({ from, to });
1160
+ }
1161
+ return years;
1162
+ }
1163
+ async function fetchContributionsForYear(graphqlFetch, login, from, to) {
1164
+ const contributionsQuery = `
1165
+ query($login: String!, $from: DateTime!, $to: DateTime!) {
1166
+ user(login: $login) {
1167
+ contributionsCollection(from: $from, to: $to) {
1168
+ commitContributionsByRepository {
1169
+ repository {
1170
+ name
1171
+ nameWithOwner
1172
+ url
1173
+ owner { login url avatarUrl __typename }
1174
+ }
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1179
+ `;
1180
+ const contributionsResp = await graphqlFetch({
1181
+ query: contributionsQuery,
1182
+ variables: { login, from, to }
1183
+ });
1184
+ return contributionsResp.data.user.contributionsCollection.commitContributionsByRepository.map((item) => item.repository).filter((repo) => repo?.nameWithOwner);
1185
+ }
1186
+ async function discoverReposFromContributions(graphqlFetch, login, repoMap) {
1187
+ console.log(`[contribkit][githubContributions] fetching contribution timeline to discover more repos...`);
1188
+ try {
1189
+ const accountCreated = await fetchUserCreationDate(graphqlFetch, login);
1190
+ const now = /* @__PURE__ */ new Date();
1191
+ const years = generateYearRanges(accountCreated, now);
1192
+ console.log(`[contribkit][githubContributions] querying contributions across ${years.length} years...`);
1193
+ for (const { from, to } of years) {
1194
+ try {
1195
+ const repos = await fetchContributionsForYear(graphqlFetch, login, from, to);
1196
+ for (const repo of repos) {
1197
+ repoMap.set(repo.nameWithOwner, repo);
1198
+ }
1199
+ } catch (e) {
1200
+ console.warn(`[contribkit][githubContributions] failed contributions query for ${from.slice(0, 4)}:`, e.message);
1201
+ }
1202
+ }
1203
+ } catch (e) {
1204
+ console.warn(`[contribkit][githubContributions] contribution timeline discovery failed:`, e.message);
1205
+ }
1206
+ }
1207
+ async function discoverReposFromMergedPRs(graphqlFetch, login, repoMap) {
1208
+ console.log(`[contribkit][githubContributions] searching for repos with merged PRs...`);
1209
+ try {
1210
+ const searchQueryBase = `is:pr is:merged author:${login}`;
1211
+ let searchAfter = null;
1212
+ let page = 0;
1213
+ const maxPages = 10;
1214
+ do {
1215
+ const response = await graphqlFetch({
1216
+ query: `
1217
+ query($searchQuery: String!, $after: String) {
1218
+ search(query: $searchQuery, type: ISSUE, first: 100, after: $after) {
1219
+ pageInfo { hasNextPage endCursor }
1220
+ edges { node { ... on PullRequest { repository { name nameWithOwner url owner { login url avatarUrl __typename } } } } }
1221
+ }
1222
+ }
1223
+ `,
1224
+ variables: { searchQuery: searchQueryBase, after: searchAfter }
1225
+ });
1226
+ for (const edge of response.data.search.edges) {
1227
+ const r = edge.node.repository;
1228
+ if (r?.nameWithOwner)
1229
+ repoMap.set(r.nameWithOwner, r);
1230
+ }
1231
+ searchAfter = response.data.search.pageInfo.endCursor;
1232
+ page++;
1233
+ if (response.data.search.pageInfo.hasNextPage && page < maxPages)
1234
+ console.log(`[contribkit][githubContributions] merged PR search page ${page}, ${repoMap.size} repos so far`);
1235
+ } while (searchAfter && page < maxPages);
1236
+ } catch (e) {
1237
+ console.warn(`[contribkit][githubContributions] merged PR search failed:`, e.message);
1238
+ }
1239
+ }
1240
+ async function fetchPRCountForRepo(graphqlFetch, repo, login) {
1241
+ const searchQuery = `repo:${repo.nameWithOwner} is:pr is:merged author:${login}`;
1242
+ try {
1243
+ const response = await graphqlFetch({
1244
+ query: `query($q: String!) { search(query: $q, type: ISSUE) { issueCount } }`,
1245
+ variables: { q: searchQuery }
1246
+ });
1247
+ return response.data.search.issueCount;
1248
+ } catch (e) {
1249
+ console.warn(`[contribkit][githubContributions] failed PR count for ${repo.nameWithOwner}:`, e.message);
1250
+ return 0;
1251
+ }
1252
+ }
1253
+ async function fetchMergedPRCounts(graphqlFetch, allRepos, login) {
1254
+ console.log(`[contribkit][githubContributions] fetching merged PR counts per repository...`);
1255
+ const repoPRs = /* @__PURE__ */ new Map();
1256
+ const batchSize = 10;
1257
+ for (let i = 0; i < allRepos.length; i += batchSize) {
1258
+ const batch = allRepos.slice(i, i + batchSize);
1259
+ const counts = await Promise.all(batch.map((repo) => fetchPRCountForRepo(graphqlFetch, repo, login)));
1260
+ for (let index = 0; index < batch.length; index++) {
1261
+ const count = counts[index];
1262
+ if (count > 0)
1263
+ repoPRs.set(batch[index].nameWithOwner, count);
1264
+ }
1265
+ if (i + batchSize < allRepos.length)
1266
+ console.log(`[contribkit][githubContributions] processed PR batches for ${Math.min(i + batchSize, allRepos.length)}/${allRepos.length} repos...`);
1267
+ }
1268
+ console.log(`[contribkit][githubContributions] found merged PR counts for ${repoPRs.size} repositories`);
1269
+ return repoPRs;
1270
+ }
1271
+ function aggregateByOwner(results) {
1272
+ const aggregated = /* @__PURE__ */ new Map();
1273
+ for (const { repo, prs } of results) {
1274
+ const key = `${repo.owner.__typename}:${repo.owner.login}`;
1275
+ const existing = aggregated.get(key);
1276
+ if (existing) {
1277
+ existing.totalPRs += prs;
1278
+ existing.repos.push({ repo, prs });
1279
+ } else {
1280
+ aggregated.set(key, { owner: repo.owner, totalPRs: prs, repos: [{ repo, prs }] });
1281
+ }
1282
+ }
1283
+ return aggregated;
1284
+ }
1285
+ function logConsolidatedOwners(aggregated) {
1286
+ const consolidated = Array.from(aggregated.values()).filter((a) => a.repos.length > 1);
1287
+ if (consolidated.length) {
1288
+ console.log(`[contribkit][githubContributions] consolidated ${consolidated.length} owners with multiple repos:`);
1289
+ for (const { owner, repos, totalPRs } of consolidated.toSorted((a, b) => b.repos.length - a.repos.length).slice(0, 10))
1290
+ console.log(` - ${owner.login}: ${repos.length} repos, ${totalPRs} merged PRs`);
1291
+ if (consolidated.length > 10)
1292
+ console.log(` ... and ${consolidated.length - 10} more`);
1293
+ }
1294
+ }
1295
+ function applyContributionScaling(totalPRs, maxContributions, logarithmicScaling) {
1296
+ let scaled = totalPRs;
1297
+ if (logarithmicScaling && scaled > 0) {
1298
+ scaled = Math.log10(scaled + 1) * 10;
1299
+ }
1300
+ if (maxContributions !== void 0 && scaled > maxContributions) {
1301
+ scaled = maxContributions;
1302
+ }
1303
+ return scaled;
1304
+ }
1305
+ function convertToSponsorships(aggregated, maxContributions, logarithmicScaling) {
1306
+ return Array.from(aggregated.values()).sort((a, b) => b.totalPRs - a.totalPRs).map(({ owner, totalPRs, repos }) => {
1307
+ const scaledPRs = applyContributionScaling(totalPRs, maxContributions, logarithmicScaling);
1308
+ const linkUrl = repos.length === 1 ? repos[0].repo.url : owner.url;
1309
+ return {
1310
+ sponsor: { type: owner.__typename, login: owner.login, name: owner.login, avatarUrl: owner.avatarUrl, linkUrl, socialLogins: { github: owner.login } },
1311
+ isOneTime: false,
1312
+ monthlyDollars: scaledPRs,
1313
+ privacyLevel: "PUBLIC",
1314
+ tierName: "Repository",
1315
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1316
+ provider: "githubContributions",
1317
+ raw: { owner, totalPRs, scaledPRs, repoCount: repos.length }
1318
+ };
1319
+ });
1320
+ }
1321
+ async function fetchGitHubContributions(token, login, maxContributions, logarithmicScaling) {
1322
+ if (!token)
1323
+ throw new Error("GitHub token is required");
1324
+ if (!login)
1325
+ throw new Error("GitHub login is required");
1326
+ const graphqlFetch = createGraphQLFetch(token);
1327
+ console.log(`[contribkit][githubContributions] discovering repositories (sources: contributionsCollection + merged PR search)...`);
1328
+ const repoMap = /* @__PURE__ */ new Map();
1329
+ await discoverReposFromContributions(graphqlFetch, login, repoMap);
1330
+ console.log(`[contribkit][githubContributions] found ${repoMap.size} repos after contribution timeline`);
1331
+ await discoverReposFromMergedPRs(graphqlFetch, login, repoMap);
1332
+ console.log(`[contribkit][githubContributions] found ${repoMap.size} repos after merged PR search`);
1333
+ const allRepos = Array.from(repoMap.values());
1334
+ console.log(`[contribkit][githubContributions] discovered ${allRepos.length} total unique repositories`);
1335
+ const repoPRs = await fetchMergedPRCounts(graphqlFetch, allRepos, login);
1336
+ const results = [];
1337
+ for (const repo of allRepos) {
1338
+ const prs = repoPRs.get(repo.nameWithOwner) || 0;
1339
+ if (prs > 0)
1340
+ results.push({ repo, prs });
1341
+ }
1342
+ console.log(`[contribkit][githubContributions] computed merged PR counts for ${results.length} repositories (from ${allRepos.length} total repos with PRs)`);
1343
+ const aggregated = aggregateByOwner(results);
1344
+ logConsolidatedOwners(aggregated);
1345
+ const scalingInfo = [];
1346
+ if (maxContributions !== void 0)
1347
+ scalingInfo.push(`max cap: ${maxContributions}`);
1348
+ if (logarithmicScaling)
1349
+ scalingInfo.push("logarithmic scaling enabled");
1350
+ if (scalingInfo.length > 0)
1351
+ console.log(`[contribkit][githubContributions] applying contribution scaling: ${scalingInfo.join(", ")}`);
1352
+ return convertToSponsorships(aggregated, maxContributions, logarithmicScaling);
1353
+ }
1354
+
1085
1355
  const GitlabContributorsProvider = {
1086
1356
  name: "gitlabContributors",
1087
1357
  fetchSponsors(config) {
@@ -1642,6 +1912,7 @@ const ProvidersMap = {
1642
1912
  polar: PolarProvider,
1643
1913
  liberapay: LiberapayProvider,
1644
1914
  githubContributors: GitHubContributorsProvider,
1915
+ githubContributions: GitHubContributionsProvider,
1645
1916
  gitlabContributors: GitlabContributorsProvider,
1646
1917
  crowdinContributors: CrowdinContributorsProvider
1647
1918
  };
@@ -1661,6 +1932,8 @@ function guessProviders(config) {
1661
1932
  items.push("liberapay");
1662
1933
  if (config.githubContributors?.login && config.githubContributors?.token)
1663
1934
  items.push("githubContributors");
1935
+ if (config.githubContributions?.login && config.githubContributions?.token)
1936
+ items.push("githubContributions");
1664
1937
  if (config.gitlabContributors?.token && config.gitlabContributors?.repoId)
1665
1938
  items.push("gitlabContributors");
1666
1939
  if (config.crowdinContributors?.token && config.crowdinContributors?.projectId)
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/LizardByte/contribkit.git"
7
7
  },
8
- "version": "2025.821.193159",
8
+ "version": "2025.1130.1103",
9
9
  "description": "Toolkit for generating contributor images",
10
10
  "license": "MIT",
11
11
  "funding": "https://app.lizardbyte.dev",