@lizardbyte/contribkit 2025.922.2626 → 2026.518.124816
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 +40 -3
- package/dist/chunks/index.mjs +56 -26
- package/dist/chunks/index3.mjs +23 -267
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +32 -1
- package/dist/index.mjs +1 -1
- package/dist/shared/{contribkit.DAlakhwL.mjs → contribkit.DYospa65.mjs} +472 -51
- package/package.json +23 -22
|
@@ -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 = "
|
|
210
|
+
const version = "2026.518.124816";
|
|
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
|
|
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}" />
|
|
@@ -588,7 +595,23 @@ async function fetchCrowdinContributors(token, projectId, minTranslations = 1) {
|
|
|
588
595
|
const DATA_URL_DEFAULT_MIME_TYPE = 'text/plain';
|
|
589
596
|
const DATA_URL_DEFAULT_CHARSET = 'us-ascii';
|
|
590
597
|
|
|
591
|
-
const
|
|
598
|
+
const encodedReservedCharactersPattern = '%(?:3A|2F|3F|23|5B|5D|40|21|24|26|27|28|29|2A|2B|2C|3B|3D)';
|
|
599
|
+
const temporaryEncodedReservedTokenBase = '__normalize_url_encoded_reserved__';
|
|
600
|
+
const temporaryEncodedReservedTokenPattern = /__normalize_url_encoded_reserved__(\d+)__/g;
|
|
601
|
+
const hasEncodedReservedCharactersRegex = new RegExp(encodedReservedCharactersPattern, 'i');
|
|
602
|
+
const encodedReservedCharactersRegex = new RegExp(encodedReservedCharactersPattern, 'gi');
|
|
603
|
+
|
|
604
|
+
const testParameter = (name, filters) => Array.isArray(filters) && filters.some(filter => {
|
|
605
|
+
if (filter instanceof RegExp) {
|
|
606
|
+
if (filter.flags.includes('g') || filter.flags.includes('y')) {
|
|
607
|
+
return new RegExp(filter.source, filter.flags.replaceAll(/[gy]/g, '')).test(name);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return filter.test(name);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return filter === name;
|
|
614
|
+
});
|
|
592
615
|
|
|
593
616
|
const supportedProtocols = new Set([
|
|
594
617
|
'https:',
|
|
@@ -596,16 +619,155 @@ const supportedProtocols = new Set([
|
|
|
596
619
|
'file:',
|
|
597
620
|
]);
|
|
598
621
|
|
|
599
|
-
const
|
|
622
|
+
const normalizeCustomProtocolOption = protocol => {
|
|
623
|
+
if (typeof protocol !== 'string') {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const normalizedProtocol = protocol.trim().toLowerCase().replace(/:$/, '');
|
|
628
|
+
return normalizedProtocol === '' ? undefined : `${normalizedProtocol}:`;
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const getCustomProtocol = urlString => {
|
|
600
632
|
try {
|
|
601
633
|
const {protocol} = new URL(urlString);
|
|
634
|
+
const hasAuthority = urlString.slice(0, protocol.length + 2).toLowerCase() === `${protocol}//`;
|
|
602
635
|
|
|
603
|
-
|
|
604
|
-
&& !protocol.includes('.')
|
|
605
|
-
&& !supportedProtocols.has(protocol)
|
|
636
|
+
if (protocol.endsWith(':')
|
|
637
|
+
&& (!protocol.includes('.') || hasAuthority)
|
|
638
|
+
&& !supportedProtocols.has(protocol)) {
|
|
639
|
+
return protocol;
|
|
640
|
+
}
|
|
641
|
+
} catch {}
|
|
642
|
+
|
|
643
|
+
return undefined;
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const decodeQueryKey = value => {
|
|
647
|
+
try {
|
|
648
|
+
return decodeURIComponent(value.replaceAll('+', '%20'));
|
|
606
649
|
} catch {
|
|
607
|
-
|
|
650
|
+
// Match URLSearchParams behavior for malformed percent-encoding.
|
|
651
|
+
return new URLSearchParams(`${value}=`).keys().next().value;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const getKeysWithoutEquals = search => {
|
|
656
|
+
const keys = new Set();
|
|
657
|
+
if (!search) {
|
|
658
|
+
return keys;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (const part of search.slice(1).split('&')) {
|
|
662
|
+
if (part && !part.includes('=')) {
|
|
663
|
+
keys.add(decodeQueryKey(part));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return keys;
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const getTemporaryEncodedReservedTokenPrefix = search => {
|
|
671
|
+
let decodedSearch = search;
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
decodedSearch = decodeURIComponent(search);
|
|
675
|
+
} catch {
|
|
676
|
+
decodedSearch = new URLSearchParams(search).toString();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const getUsedTokenIndexes = value => {
|
|
680
|
+
const indexes = new Set();
|
|
681
|
+
|
|
682
|
+
for (const match of value.matchAll(temporaryEncodedReservedTokenPattern)) {
|
|
683
|
+
indexes.add(Number.parseInt(match[1], 10));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return indexes;
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const usedTokenIndexes = getUsedTokenIndexes(search);
|
|
690
|
+
for (const tokenIndex of getUsedTokenIndexes(decodedSearch)) {
|
|
691
|
+
usedTokenIndexes.add(tokenIndex);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
let tokenIndex = 0;
|
|
695
|
+
while (usedTokenIndexes.has(tokenIndex)) {
|
|
696
|
+
tokenIndex++;
|
|
608
697
|
}
|
|
698
|
+
|
|
699
|
+
return `${temporaryEncodedReservedTokenBase}${tokenIndex}__`;
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const sortSearchParameters = (searchParameters, encodedReservedTokenRegex) => {
|
|
703
|
+
if (!encodedReservedTokenRegex) {
|
|
704
|
+
searchParameters.sort();
|
|
705
|
+
return searchParameters.toString();
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const getSortableKey = key => key.replace(encodedReservedTokenRegex, (_, hexCode) => String.fromCodePoint(Number.parseInt(hexCode, 16)));
|
|
709
|
+
const entries = [...searchParameters.entries()];
|
|
710
|
+
entries.sort(([leftKey], [rightKey]) => {
|
|
711
|
+
const left = getSortableKey(leftKey);
|
|
712
|
+
const right = getSortableKey(rightKey);
|
|
713
|
+
return left < right ? -1 : (left > right ? 1 : 0);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
return new URLSearchParams(entries).toString();
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const decodeReservedTokens = (value, encodedReservedTokenRegex) => {
|
|
720
|
+
if (!encodedReservedTokenRegex) {
|
|
721
|
+
return value;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return value.replace(encodedReservedTokenRegex, (_, hexCode) => String.fromCodePoint(Number.parseInt(hexCode, 16)));
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const normalizeEmptyQueryParameters = (search, emptyQueryValue, originalSearch) => {
|
|
728
|
+
const isAlways = emptyQueryValue === 'always';
|
|
729
|
+
const isNever = emptyQueryValue === 'never';
|
|
730
|
+
const keysWithoutEquals = (isAlways || isNever) ? undefined : getKeysWithoutEquals(originalSearch);
|
|
731
|
+
|
|
732
|
+
const normalizeKey = key => key.replaceAll('+', '%20');
|
|
733
|
+
const formatEmptyValue = normalizedKey => {
|
|
734
|
+
if (isAlways) {
|
|
735
|
+
return `${normalizedKey}=`;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (isNever) {
|
|
739
|
+
return normalizedKey;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return keysWithoutEquals.has(decodeQueryKey(normalizedKey)) ? normalizedKey : `${normalizedKey}=`;
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const normalizeParameter = parameter => {
|
|
746
|
+
const equalIndex = parameter.indexOf('=');
|
|
747
|
+
|
|
748
|
+
if (equalIndex === -1) {
|
|
749
|
+
// Normalize + to %20 (+ means space in query strings)
|
|
750
|
+
return formatEmptyValue(normalizeKey(parameter));
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const key = parameter.slice(0, equalIndex);
|
|
754
|
+
const value = parameter.slice(equalIndex + 1);
|
|
755
|
+
|
|
756
|
+
if (value === '') {
|
|
757
|
+
if (key === '') {
|
|
758
|
+
return '=';
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Normalize + to %20 (+ means space in query strings)
|
|
762
|
+
return formatEmptyValue(normalizeKey(key));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Normalize + to %20 in key.
|
|
766
|
+
return `${normalizeKey(key)}=${value}`;
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
const parameters = search.slice(1).split('&').filter(Boolean);
|
|
770
|
+
return parameters.length === 0 ? '' : `?${parameters.map(x => normalizeParameter(x)).join('&')}`;
|
|
609
771
|
};
|
|
610
772
|
|
|
611
773
|
const normalizeDataURL = (urlString, {stripHash}) => {
|
|
@@ -615,18 +777,16 @@ const normalizeDataURL = (urlString, {stripHash}) => {
|
|
|
615
777
|
throw new Error(`Invalid URL: ${urlString}`);
|
|
616
778
|
}
|
|
617
779
|
|
|
618
|
-
|
|
780
|
+
const {type, data, hash} = match.groups;
|
|
619
781
|
const mediaType = type.split(';');
|
|
620
|
-
hash = stripHash ? '' : hash;
|
|
621
782
|
|
|
622
|
-
|
|
623
|
-
if (
|
|
783
|
+
const isBase64 = mediaType.at(-1) === 'base64';
|
|
784
|
+
if (isBase64) {
|
|
624
785
|
mediaType.pop();
|
|
625
|
-
isBase64 = true;
|
|
626
786
|
}
|
|
627
787
|
|
|
628
788
|
// Lowercase MIME type
|
|
629
|
-
const mimeType = mediaType.shift()
|
|
789
|
+
const mimeType = mediaType.shift().toLowerCase();
|
|
630
790
|
const attributes = mediaType
|
|
631
791
|
.map(attribute => {
|
|
632
792
|
let [key, value = ''] = attribute.split('=').map(string => string.trim());
|
|
@@ -644,9 +804,7 @@ const normalizeDataURL = (urlString, {stripHash}) => {
|
|
|
644
804
|
})
|
|
645
805
|
.filter(Boolean);
|
|
646
806
|
|
|
647
|
-
const normalizedMediaType = [
|
|
648
|
-
...attributes,
|
|
649
|
-
];
|
|
807
|
+
const normalizedMediaType = [...attributes];
|
|
650
808
|
|
|
651
809
|
if (isBase64) {
|
|
652
810
|
normalizedMediaType.push('base64');
|
|
@@ -656,7 +814,8 @@ const normalizeDataURL = (urlString, {stripHash}) => {
|
|
|
656
814
|
normalizedMediaType.unshift(mimeType);
|
|
657
815
|
}
|
|
658
816
|
|
|
659
|
-
|
|
817
|
+
const hashPart = stripHash || !hash ? '' : `#${hash}`;
|
|
818
|
+
return `data:${normalizedMediaType.join(';')},${isBase64 ? data.trim() : data}${hashPart}`;
|
|
660
819
|
};
|
|
661
820
|
|
|
662
821
|
function normalizeUrl$1(urlString, options) {
|
|
@@ -677,6 +836,7 @@ function normalizeUrl$1(urlString, options) {
|
|
|
677
836
|
sortQueryParameters: true,
|
|
678
837
|
removePath: false,
|
|
679
838
|
transformPath: false,
|
|
839
|
+
emptyQueryValue: 'preserve',
|
|
680
840
|
...options,
|
|
681
841
|
};
|
|
682
842
|
|
|
@@ -692,7 +852,13 @@ function normalizeUrl$1(urlString, options) {
|
|
|
692
852
|
return normalizeDataURL(urlString, options);
|
|
693
853
|
}
|
|
694
854
|
|
|
695
|
-
|
|
855
|
+
const customProtocols = Array.isArray(options.customProtocols) ? options.customProtocols : [];
|
|
856
|
+
const normalizedCustomProtocols = new Set(customProtocols
|
|
857
|
+
.map(protocol => normalizeCustomProtocolOption(protocol))
|
|
858
|
+
.filter(Boolean));
|
|
859
|
+
|
|
860
|
+
const customProtocol = getCustomProtocol(urlString);
|
|
861
|
+
if (customProtocol && !normalizedCustomProtocols.has(customProtocol)) {
|
|
696
862
|
return urlString;
|
|
697
863
|
}
|
|
698
864
|
|
|
@@ -700,7 +866,7 @@ function normalizeUrl$1(urlString, options) {
|
|
|
700
866
|
const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString);
|
|
701
867
|
|
|
702
868
|
// Prepend protocol
|
|
703
|
-
if (!isRelativeUrl) {
|
|
869
|
+
if (!isRelativeUrl && !customProtocol) {
|
|
704
870
|
urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol);
|
|
705
871
|
}
|
|
706
872
|
|
|
@@ -755,13 +921,13 @@ function normalizeUrl$1(urlString, options) {
|
|
|
755
921
|
const protocolAtIndex = match.index;
|
|
756
922
|
const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex);
|
|
757
923
|
|
|
758
|
-
result += intermediate.
|
|
924
|
+
result += intermediate.replaceAll(/\/{2,}/g, '/');
|
|
759
925
|
result += protocol;
|
|
760
926
|
lastIndex = protocolAtIndex + protocol.length;
|
|
761
927
|
}
|
|
762
928
|
|
|
763
|
-
const remnant = urlObject.pathname.slice(lastIndex
|
|
764
|
-
result += remnant.
|
|
929
|
+
const remnant = urlObject.pathname.slice(lastIndex);
|
|
930
|
+
result += remnant.replaceAll(/\/{2,}/g, '/');
|
|
765
931
|
|
|
766
932
|
urlObject.pathname = result;
|
|
767
933
|
}
|
|
@@ -769,7 +935,7 @@ function normalizeUrl$1(urlString, options) {
|
|
|
769
935
|
// Decode URI octets
|
|
770
936
|
if (urlObject.pathname) {
|
|
771
937
|
try {
|
|
772
|
-
urlObject.pathname = decodeURI(urlObject.pathname).
|
|
938
|
+
urlObject.pathname = decodeURI(urlObject.pathname).replaceAll('\\', '%5C');
|
|
773
939
|
} catch {}
|
|
774
940
|
}
|
|
775
941
|
|
|
@@ -779,12 +945,12 @@ function normalizeUrl$1(urlString, options) {
|
|
|
779
945
|
}
|
|
780
946
|
|
|
781
947
|
if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) {
|
|
782
|
-
|
|
783
|
-
const lastComponent = pathComponents
|
|
948
|
+
const pathComponents = urlObject.pathname.split('/').filter(Boolean);
|
|
949
|
+
const lastComponent = pathComponents.at(-1);
|
|
784
950
|
|
|
785
|
-
if (testParameter(lastComponent, options.removeDirectoryIndex)) {
|
|
786
|
-
pathComponents
|
|
787
|
-
urlObject.pathname = pathComponents.
|
|
951
|
+
if (lastComponent && testParameter(lastComponent, options.removeDirectoryIndex)) {
|
|
952
|
+
pathComponents.pop();
|
|
953
|
+
urlObject.pathname = pathComponents.length > 0 ? `/${pathComponents.join('/')}/` : '/';
|
|
788
954
|
}
|
|
789
955
|
}
|
|
790
956
|
|
|
@@ -814,49 +980,61 @@ function normalizeUrl$1(urlString, options) {
|
|
|
814
980
|
}
|
|
815
981
|
}
|
|
816
982
|
|
|
983
|
+
// Capture original query params format before any searchParams modifications
|
|
984
|
+
const originalSearch = urlObject.search;
|
|
985
|
+
let encodedReservedTokenRegex;
|
|
986
|
+
|
|
987
|
+
if (options.sortQueryParameters && hasEncodedReservedCharactersRegex.test(originalSearch)) {
|
|
988
|
+
const encodedReservedTokenPrefix = getTemporaryEncodedReservedTokenPrefix(originalSearch);
|
|
989
|
+
urlObject.search = originalSearch.replaceAll(encodedReservedCharactersRegex, match => `${encodedReservedTokenPrefix}${match.slice(1).toUpperCase()}`);
|
|
990
|
+
encodedReservedTokenRegex = new RegExp(`${encodedReservedTokenPrefix}([0-9A-F]{2})`, 'g');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const hasKeepQueryParameters = Array.isArray(options.keepQueryParameters);
|
|
994
|
+
const {searchParams} = urlObject;
|
|
995
|
+
|
|
817
996
|
// Remove query unwanted parameters
|
|
818
|
-
if (Array.isArray(options.removeQueryParameters)) {
|
|
997
|
+
if (!hasKeepQueryParameters && Array.isArray(options.removeQueryParameters) && options.removeQueryParameters.length > 0) {
|
|
819
998
|
// eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.
|
|
820
|
-
for (const key of [...
|
|
821
|
-
if (testParameter(key, options.removeQueryParameters)) {
|
|
822
|
-
|
|
999
|
+
for (const key of [...searchParams.keys()]) {
|
|
1000
|
+
if (testParameter(decodeReservedTokens(key, encodedReservedTokenRegex), options.removeQueryParameters)) {
|
|
1001
|
+
searchParams.delete(key);
|
|
823
1002
|
}
|
|
824
1003
|
}
|
|
825
1004
|
}
|
|
826
1005
|
|
|
827
|
-
if (!
|
|
1006
|
+
if (!hasKeepQueryParameters && options.removeQueryParameters === true) {
|
|
828
1007
|
urlObject.search = '';
|
|
829
1008
|
}
|
|
830
1009
|
|
|
831
1010
|
// Keep wanted query parameters
|
|
832
|
-
if (
|
|
1011
|
+
if (hasKeepQueryParameters && options.keepQueryParameters.length > 0) {
|
|
833
1012
|
// eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.
|
|
834
|
-
for (const key of [...
|
|
835
|
-
if (!testParameter(key, options.keepQueryParameters)) {
|
|
836
|
-
|
|
1013
|
+
for (const key of [...searchParams.keys()]) {
|
|
1014
|
+
if (!testParameter(decodeReservedTokens(key, encodedReservedTokenRegex), options.keepQueryParameters)) {
|
|
1015
|
+
searchParams.delete(key);
|
|
837
1016
|
}
|
|
838
1017
|
}
|
|
1018
|
+
} else if (hasKeepQueryParameters) {
|
|
1019
|
+
urlObject.search = '';
|
|
839
1020
|
}
|
|
840
1021
|
|
|
841
1022
|
// Sort query parameters
|
|
842
1023
|
if (options.sortQueryParameters) {
|
|
843
|
-
|
|
844
|
-
urlObject.searchParams.sort();
|
|
1024
|
+
urlObject.search = sortSearchParameters(urlObject.searchParams, encodedReservedTokenRegex);
|
|
845
1025
|
|
|
846
|
-
//
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
} catch {}
|
|
1026
|
+
// Sorting and serializing encode the search parameters, so we need to decode them again.
|
|
1027
|
+
// Protect &%#? and %2B from decoding (would break URL structure or change meaning) by double-encoding them first.
|
|
1028
|
+
urlObject.search = decodeURIComponent(urlObject.search.replaceAll(/%(?:26|23|3f|25|2b)/gi, match => `%25${match.slice(1)}`));
|
|
850
1029
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
for (const part of partsWithoutEquals) {
|
|
854
|
-
const decoded = decodeURIComponent(part);
|
|
855
|
-
// Only replace at word boundaries to avoid partial matches
|
|
856
|
-
urlObject.search = urlObject.search.replace(`?${decoded}=`, `?${decoded}`).replace(`&${decoded}=`, `&${decoded}`);
|
|
1030
|
+
if (encodedReservedTokenRegex) {
|
|
1031
|
+
urlObject.search = urlObject.search.replace(encodedReservedTokenRegex, '%$1');
|
|
857
1032
|
}
|
|
858
1033
|
}
|
|
859
1034
|
|
|
1035
|
+
// Normalize empty query parameter values
|
|
1036
|
+
urlObject.search = normalizeEmptyQueryParameters(urlObject.search, options.emptyQueryValue, originalSearch);
|
|
1037
|
+
|
|
860
1038
|
if (options.removeTrailingSlash) {
|
|
861
1039
|
urlObject.pathname = urlObject.pathname.replace(/\/$/, '');
|
|
862
1040
|
}
|
|
@@ -1105,6 +1283,246 @@ async function fetchGitHubContributors(token, login, repo, minContributions = 1)
|
|
|
1105
1283
|
}));
|
|
1106
1284
|
}
|
|
1107
1285
|
|
|
1286
|
+
const GitHubContributionsProvider = {
|
|
1287
|
+
name: "githubContributions",
|
|
1288
|
+
fetchSponsors(config) {
|
|
1289
|
+
if (!config.githubContributions?.login)
|
|
1290
|
+
throw new Error("GitHub login is required for githubContributions provider");
|
|
1291
|
+
return fetchGitHubContributions(
|
|
1292
|
+
config.githubContributions?.token || config.token,
|
|
1293
|
+
config.githubContributions.login,
|
|
1294
|
+
config.githubContributions.maxContributions,
|
|
1295
|
+
config.githubContributions.logarithmicScaling
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
function createGraphQLFetch(token) {
|
|
1300
|
+
return async (body) => {
|
|
1301
|
+
return await $fetch("https://api.github.com/graphql", {
|
|
1302
|
+
method: "POST",
|
|
1303
|
+
headers: {
|
|
1304
|
+
Authorization: `bearer ${token}`,
|
|
1305
|
+
"Content-Type": "application/json"
|
|
1306
|
+
},
|
|
1307
|
+
body
|
|
1308
|
+
});
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
async function fetchUserCreationDate(graphqlFetch, login) {
|
|
1312
|
+
const userInfoQuery = `
|
|
1313
|
+
query($login: String!) {
|
|
1314
|
+
user(login: $login) {
|
|
1315
|
+
createdAt
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
`;
|
|
1319
|
+
const userInfo = await graphqlFetch({
|
|
1320
|
+
query: userInfoQuery,
|
|
1321
|
+
variables: { login }
|
|
1322
|
+
});
|
|
1323
|
+
return new Date(userInfo.data.user.createdAt);
|
|
1324
|
+
}
|
|
1325
|
+
function generateYearRanges(accountCreated, now) {
|
|
1326
|
+
const years = [];
|
|
1327
|
+
for (let year = accountCreated.getFullYear(); year <= now.getFullYear(); year++) {
|
|
1328
|
+
const from = year === accountCreated.getFullYear() ? accountCreated.toISOString() : `${year}-01-01T00:00:00Z`;
|
|
1329
|
+
const to = year === now.getFullYear() ? now.toISOString() : `${year}-12-31T23:59:59Z`;
|
|
1330
|
+
years.push({ from, to });
|
|
1331
|
+
}
|
|
1332
|
+
return years;
|
|
1333
|
+
}
|
|
1334
|
+
async function fetchContributionsForYear(graphqlFetch, login, from, to) {
|
|
1335
|
+
const contributionsQuery = `
|
|
1336
|
+
query($login: String!, $from: DateTime!, $to: DateTime!) {
|
|
1337
|
+
user(login: $login) {
|
|
1338
|
+
contributionsCollection(from: $from, to: $to) {
|
|
1339
|
+
commitContributionsByRepository {
|
|
1340
|
+
repository {
|
|
1341
|
+
name
|
|
1342
|
+
nameWithOwner
|
|
1343
|
+
url
|
|
1344
|
+
owner { login url avatarUrl __typename }
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
`;
|
|
1351
|
+
const contributionsResp = await graphqlFetch({
|
|
1352
|
+
query: contributionsQuery,
|
|
1353
|
+
variables: { login, from, to }
|
|
1354
|
+
});
|
|
1355
|
+
return contributionsResp.data.user.contributionsCollection.commitContributionsByRepository.map((item) => item.repository).filter((repo) => repo?.nameWithOwner);
|
|
1356
|
+
}
|
|
1357
|
+
async function discoverReposFromContributions(graphqlFetch, login, repoMap) {
|
|
1358
|
+
console.log(`[contribkit][githubContributions] fetching contribution timeline to discover more repos...`);
|
|
1359
|
+
try {
|
|
1360
|
+
const accountCreated = await fetchUserCreationDate(graphqlFetch, login);
|
|
1361
|
+
const now = /* @__PURE__ */ new Date();
|
|
1362
|
+
const years = generateYearRanges(accountCreated, now);
|
|
1363
|
+
console.log(`[contribkit][githubContributions] querying contributions across ${years.length} years...`);
|
|
1364
|
+
for (const { from, to } of years) {
|
|
1365
|
+
try {
|
|
1366
|
+
const repos = await fetchContributionsForYear(graphqlFetch, login, from, to);
|
|
1367
|
+
for (const repo of repos) {
|
|
1368
|
+
repoMap.set(repo.nameWithOwner, repo);
|
|
1369
|
+
}
|
|
1370
|
+
} catch (e) {
|
|
1371
|
+
console.warn(`[contribkit][githubContributions] failed contributions query for ${from.slice(0, 4)}:`, e.message);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
console.warn(`[contribkit][githubContributions] contribution timeline discovery failed:`, e.message);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
async function discoverReposFromMergedPRs(graphqlFetch, login, repoMap) {
|
|
1379
|
+
console.log(`[contribkit][githubContributions] searching for repos with merged PRs...`);
|
|
1380
|
+
try {
|
|
1381
|
+
const searchQueryBase = `is:pr is:merged author:${login}`;
|
|
1382
|
+
let searchAfter = null;
|
|
1383
|
+
let page = 0;
|
|
1384
|
+
const maxPages = 10;
|
|
1385
|
+
do {
|
|
1386
|
+
const response = await graphqlFetch({
|
|
1387
|
+
query: `
|
|
1388
|
+
query($searchQuery: String!, $after: String) {
|
|
1389
|
+
search(query: $searchQuery, type: ISSUE, first: 100, after: $after) {
|
|
1390
|
+
pageInfo { hasNextPage endCursor }
|
|
1391
|
+
edges { node { ... on PullRequest { repository { name nameWithOwner url owner { login url avatarUrl __typename } } } } }
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
`,
|
|
1395
|
+
variables: { searchQuery: searchQueryBase, after: searchAfter }
|
|
1396
|
+
});
|
|
1397
|
+
for (const edge of response.data.search.edges) {
|
|
1398
|
+
const r = edge.node.repository;
|
|
1399
|
+
if (r?.nameWithOwner)
|
|
1400
|
+
repoMap.set(r.nameWithOwner, r);
|
|
1401
|
+
}
|
|
1402
|
+
searchAfter = response.data.search.pageInfo.endCursor;
|
|
1403
|
+
page++;
|
|
1404
|
+
if (response.data.search.pageInfo.hasNextPage && page < maxPages)
|
|
1405
|
+
console.log(`[contribkit][githubContributions] merged PR search page ${page}, ${repoMap.size} repos so far`);
|
|
1406
|
+
} while (searchAfter && page < maxPages);
|
|
1407
|
+
} catch (e) {
|
|
1408
|
+
console.warn(`[contribkit][githubContributions] merged PR search failed:`, e.message);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
async function fetchPRCountForRepo(graphqlFetch, repo, login) {
|
|
1412
|
+
const searchQuery = `repo:${repo.nameWithOwner} is:pr is:merged author:${login}`;
|
|
1413
|
+
try {
|
|
1414
|
+
const response = await graphqlFetch({
|
|
1415
|
+
query: `query($q: String!) { search(query: $q, type: ISSUE) { issueCount } }`,
|
|
1416
|
+
variables: { q: searchQuery }
|
|
1417
|
+
});
|
|
1418
|
+
return response.data.search.issueCount;
|
|
1419
|
+
} catch (e) {
|
|
1420
|
+
console.warn(`[contribkit][githubContributions] failed PR count for ${repo.nameWithOwner}:`, e.message);
|
|
1421
|
+
return 0;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
async function fetchMergedPRCounts(graphqlFetch, allRepos, login) {
|
|
1425
|
+
console.log(`[contribkit][githubContributions] fetching merged PR counts per repository...`);
|
|
1426
|
+
const repoPRs = /* @__PURE__ */ new Map();
|
|
1427
|
+
const batchSize = 10;
|
|
1428
|
+
for (let i = 0; i < allRepos.length; i += batchSize) {
|
|
1429
|
+
const batch = allRepos.slice(i, i + batchSize);
|
|
1430
|
+
const counts = await Promise.all(batch.map((repo) => fetchPRCountForRepo(graphqlFetch, repo, login)));
|
|
1431
|
+
for (let index = 0; index < batch.length; index++) {
|
|
1432
|
+
const count = counts[index];
|
|
1433
|
+
if (count > 0)
|
|
1434
|
+
repoPRs.set(batch[index].nameWithOwner, count);
|
|
1435
|
+
}
|
|
1436
|
+
if (i + batchSize < allRepos.length)
|
|
1437
|
+
console.log(`[contribkit][githubContributions] processed PR batches for ${Math.min(i + batchSize, allRepos.length)}/${allRepos.length} repos...`);
|
|
1438
|
+
}
|
|
1439
|
+
console.log(`[contribkit][githubContributions] found merged PR counts for ${repoPRs.size} repositories`);
|
|
1440
|
+
return repoPRs;
|
|
1441
|
+
}
|
|
1442
|
+
function aggregateByOwner(results) {
|
|
1443
|
+
const aggregated = /* @__PURE__ */ new Map();
|
|
1444
|
+
for (const { repo, prs } of results) {
|
|
1445
|
+
const key = `${repo.owner.__typename}:${repo.owner.login}`;
|
|
1446
|
+
const existing = aggregated.get(key);
|
|
1447
|
+
if (existing) {
|
|
1448
|
+
existing.totalPRs += prs;
|
|
1449
|
+
existing.repos.push({ repo, prs });
|
|
1450
|
+
} else {
|
|
1451
|
+
aggregated.set(key, { owner: repo.owner, totalPRs: prs, repos: [{ repo, prs }] });
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return aggregated;
|
|
1455
|
+
}
|
|
1456
|
+
function logConsolidatedOwners(aggregated) {
|
|
1457
|
+
const consolidated = Array.from(aggregated.values()).filter((a) => a.repos.length > 1);
|
|
1458
|
+
if (consolidated.length) {
|
|
1459
|
+
console.log(`[contribkit][githubContributions] consolidated ${consolidated.length} owners with multiple repos:`);
|
|
1460
|
+
for (const { owner, repos, totalPRs } of consolidated.toSorted((a, b) => b.repos.length - a.repos.length).slice(0, 10))
|
|
1461
|
+
console.log(` - ${owner.login}: ${repos.length} repos, ${totalPRs} merged PRs`);
|
|
1462
|
+
if (consolidated.length > 10)
|
|
1463
|
+
console.log(` ... and ${consolidated.length - 10} more`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
function applyContributionScaling(totalPRs, maxContributions, logarithmicScaling) {
|
|
1467
|
+
let scaled = totalPRs;
|
|
1468
|
+
if (logarithmicScaling && scaled > 0) {
|
|
1469
|
+
scaled = Math.log10(scaled + 1) * 10;
|
|
1470
|
+
}
|
|
1471
|
+
if (maxContributions !== void 0 && scaled > maxContributions) {
|
|
1472
|
+
scaled = maxContributions;
|
|
1473
|
+
}
|
|
1474
|
+
return scaled;
|
|
1475
|
+
}
|
|
1476
|
+
function convertToSponsorships(aggregated, maxContributions, logarithmicScaling) {
|
|
1477
|
+
return Array.from(aggregated.values()).sort((a, b) => b.totalPRs - a.totalPRs).map(({ owner, totalPRs, repos }) => {
|
|
1478
|
+
const scaledPRs = applyContributionScaling(totalPRs, maxContributions, logarithmicScaling);
|
|
1479
|
+
const linkUrl = repos.length === 1 ? repos[0].repo.url : owner.url;
|
|
1480
|
+
return {
|
|
1481
|
+
sponsor: { type: owner.__typename, login: owner.login, name: owner.login, avatarUrl: owner.avatarUrl, linkUrl, socialLogins: { github: owner.login } },
|
|
1482
|
+
isOneTime: false,
|
|
1483
|
+
monthlyDollars: scaledPRs,
|
|
1484
|
+
privacyLevel: "PUBLIC",
|
|
1485
|
+
tierName: "Repository",
|
|
1486
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1487
|
+
provider: "githubContributions",
|
|
1488
|
+
raw: { owner, totalPRs, scaledPRs, repoCount: repos.length }
|
|
1489
|
+
};
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
async function fetchGitHubContributions(token, login, maxContributions, logarithmicScaling) {
|
|
1493
|
+
if (!token)
|
|
1494
|
+
throw new Error("GitHub token is required");
|
|
1495
|
+
if (!login)
|
|
1496
|
+
throw new Error("GitHub login is required");
|
|
1497
|
+
const graphqlFetch = createGraphQLFetch(token);
|
|
1498
|
+
console.log(`[contribkit][githubContributions] discovering repositories (sources: contributionsCollection + merged PR search)...`);
|
|
1499
|
+
const repoMap = /* @__PURE__ */ new Map();
|
|
1500
|
+
await discoverReposFromContributions(graphqlFetch, login, repoMap);
|
|
1501
|
+
console.log(`[contribkit][githubContributions] found ${repoMap.size} repos after contribution timeline`);
|
|
1502
|
+
await discoverReposFromMergedPRs(graphqlFetch, login, repoMap);
|
|
1503
|
+
console.log(`[contribkit][githubContributions] found ${repoMap.size} repos after merged PR search`);
|
|
1504
|
+
const allRepos = Array.from(repoMap.values());
|
|
1505
|
+
console.log(`[contribkit][githubContributions] discovered ${allRepos.length} total unique repositories`);
|
|
1506
|
+
const repoPRs = await fetchMergedPRCounts(graphqlFetch, allRepos, login);
|
|
1507
|
+
const results = [];
|
|
1508
|
+
for (const repo of allRepos) {
|
|
1509
|
+
const prs = repoPRs.get(repo.nameWithOwner) || 0;
|
|
1510
|
+
if (prs > 0)
|
|
1511
|
+
results.push({ repo, prs });
|
|
1512
|
+
}
|
|
1513
|
+
console.log(`[contribkit][githubContributions] computed merged PR counts for ${results.length} repositories (from ${allRepos.length} total repos with PRs)`);
|
|
1514
|
+
const aggregated = aggregateByOwner(results);
|
|
1515
|
+
logConsolidatedOwners(aggregated);
|
|
1516
|
+
const scalingInfo = [];
|
|
1517
|
+
if (maxContributions !== void 0)
|
|
1518
|
+
scalingInfo.push(`max cap: ${maxContributions}`);
|
|
1519
|
+
if (logarithmicScaling)
|
|
1520
|
+
scalingInfo.push("logarithmic scaling enabled");
|
|
1521
|
+
if (scalingInfo.length > 0)
|
|
1522
|
+
console.log(`[contribkit][githubContributions] applying contribution scaling: ${scalingInfo.join(", ")}`);
|
|
1523
|
+
return convertToSponsorships(aggregated, maxContributions, logarithmicScaling);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1108
1526
|
const GitlabContributorsProvider = {
|
|
1109
1527
|
name: "gitlabContributors",
|
|
1110
1528
|
fetchSponsors(config) {
|
|
@@ -1665,6 +2083,7 @@ const ProvidersMap = {
|
|
|
1665
2083
|
polar: PolarProvider,
|
|
1666
2084
|
liberapay: LiberapayProvider,
|
|
1667
2085
|
githubContributors: GitHubContributorsProvider,
|
|
2086
|
+
githubContributions: GitHubContributionsProvider,
|
|
1668
2087
|
gitlabContributors: GitlabContributorsProvider,
|
|
1669
2088
|
crowdinContributors: CrowdinContributorsProvider
|
|
1670
2089
|
};
|
|
@@ -1684,6 +2103,8 @@ function guessProviders(config) {
|
|
|
1684
2103
|
items.push("liberapay");
|
|
1685
2104
|
if (config.githubContributors?.login && config.githubContributors?.token)
|
|
1686
2105
|
items.push("githubContributors");
|
|
2106
|
+
if (config.githubContributions?.login && config.githubContributions?.token)
|
|
2107
|
+
items.push("githubContributions");
|
|
1687
2108
|
if (config.gitlabContributors?.token && config.gitlabContributors?.repoId)
|
|
1688
2109
|
items.push("gitlabContributors");
|
|
1689
2110
|
if (config.crowdinContributors?.token && config.crowdinContributors?.projectId)
|
|
@@ -1713,4 +2134,4 @@ async function fetchSponsors(config) {
|
|
|
1713
2134
|
|
|
1714
2135
|
const outputFormats = ["svg", "png", "webp", "json"];
|
|
1715
2136
|
|
|
1716
|
-
export { FALLBACK_AVATAR as F, GitHubProvider as G, ProvidersMap as P, SvgComposer as S,
|
|
2137
|
+
export { FALLBACK_AVATAR as F, GitHubProvider as G, ProvidersMap as P, SvgComposer as S, defaultInlineCSS as a, defaultTiers as b, defineConfig as c, defaultConfig as d, fetchSponsors as e, fetchGitHubSponsors as f, genSvgImage as g, generateBadge as h, guessProviders as i, presets as j, resolveAvatars as k, loadConfig as l, makeQuery as m, resolveProviders as n, outputFormats as o, partitionTiers as p, svgToWebp as q, resizeImage as r, svgToPng as s, tierPresets as t, version as v };
|