@stats-organization/github-readme-stats-core 2.1.4 → 2.2.0

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.
Files changed (182) hide show
  1. package/build/api/gist.d.ts +2 -7
  2. package/build/api/gist.d.ts.map +1 -1
  3. package/build/api/gist.js +28 -26
  4. package/build/api/index.d.ts +3 -8
  5. package/build/api/index.d.ts.map +1 -1
  6. package/build/api/index.js +32 -36
  7. package/build/api/pin.d.ts +2 -7
  8. package/build/api/pin.d.ts.map +1 -1
  9. package/build/api/pin.js +18 -33
  10. package/build/api/top-langs.d.ts +2 -7
  11. package/build/api/top-langs.d.ts.map +1 -1
  12. package/build/api/top-langs.js +32 -44
  13. package/build/api/wakatime.d.ts +2 -7
  14. package/build/api/wakatime.d.ts.map +1 -1
  15. package/build/api/wakatime.js +28 -26
  16. package/build/calculateRank.d.ts +12 -11
  17. package/build/calculateRank.d.ts.map +1 -1
  18. package/build/calculateRank.js +18 -18
  19. package/build/cards/common-options.d.ts +13 -0
  20. package/build/cards/common-options.d.ts.map +1 -0
  21. package/build/cards/common-options.js +1 -0
  22. package/build/cards/gist.d.ts +11 -16
  23. package/build/cards/gist.d.ts.map +1 -1
  24. package/build/cards/gist.js +26 -31
  25. package/build/cards/repo.d.ts +19 -16
  26. package/build/cards/repo.d.ts.map +1 -1
  27. package/build/cards/repo.js +79 -72
  28. package/build/cards/stats.d.ts +29 -44
  29. package/build/cards/stats.d.ts.map +1 -1
  30. package/build/cards/stats.js +118 -173
  31. package/build/cards/top-languages.d.ts +81 -77
  32. package/build/cards/top-languages.d.ts.map +1 -1
  33. package/build/cards/top-languages.js +186 -184
  34. package/build/cards/wakatime.d.ts +21 -11
  35. package/build/cards/wakatime.d.ts.map +1 -1
  36. package/build/cards/wakatime.js +92 -109
  37. package/build/common/Card.d.ts +41 -14
  38. package/build/common/Card.d.ts.map +1 -1
  39. package/build/common/Card.js +92 -31
  40. package/build/common/I18n.d.ts +4 -2
  41. package/build/common/I18n.d.ts.map +1 -1
  42. package/build/common/color.d.ts +91 -15
  43. package/build/common/color.d.ts.map +1 -1
  44. package/build/common/color.js +163 -17
  45. package/build/common/config.d.ts +2 -1
  46. package/build/common/config.d.ts.map +1 -1
  47. package/build/common/config.js +16 -26
  48. package/build/common/constants.d.ts +2 -1
  49. package/build/common/constants.d.ts.map +1 -1
  50. package/build/common/date.d.ts +37 -0
  51. package/build/common/date.d.ts.map +1 -0
  52. package/build/common/date.js +31 -0
  53. package/build/common/html.js +1 -1
  54. package/build/common/http.d.ts +37 -5
  55. package/build/common/http.d.ts.map +1 -1
  56. package/build/common/http.js +29 -6
  57. package/build/common/icons.d.ts +1 -0
  58. package/build/common/icons.d.ts.map +1 -1
  59. package/build/common/icons.js +1 -0
  60. package/build/common/languageColors.d.ts +11 -0
  61. package/build/common/languageColors.d.ts.map +1 -0
  62. package/build/common/languageColors.js +15 -0
  63. package/build/common/languageColors.json +10 -1
  64. package/build/common/ops.d.ts +27 -33
  65. package/build/common/ops.d.ts.map +1 -1
  66. package/build/common/ops.js +31 -48
  67. package/build/common/render.d.ts +154 -113
  68. package/build/common/render.d.ts.map +1 -1
  69. package/build/common/render.js +209 -92
  70. package/build/common/retryer.d.ts +15 -6
  71. package/build/common/retryer.d.ts.map +1 -1
  72. package/build/common/retryer.js +2 -0
  73. package/build/fetchers/gist.d.ts +6 -21
  74. package/build/fetchers/gist.d.ts.map +1 -1
  75. package/build/fetchers/gist.js +28 -64
  76. package/build/fetchers/repo.d.ts +12 -11
  77. package/build/fetchers/repo.d.ts.map +1 -1
  78. package/build/fetchers/repo.js +27 -68
  79. package/build/fetchers/stats.d.ts +24 -17
  80. package/build/fetchers/stats.d.ts.map +1 -1
  81. package/build/fetchers/stats.js +302 -184
  82. package/build/fetchers/top-languages.d.ts +10 -15
  83. package/build/fetchers/top-languages.d.ts.map +1 -1
  84. package/build/fetchers/top-languages.js +52 -103
  85. package/build/fetchers/types.d.ts +130 -0
  86. package/build/fetchers/types.d.ts.map +1 -0
  87. package/build/fetchers/types.js +1 -0
  88. package/build/fetchers/wakatime.d.ts +9 -5
  89. package/build/fetchers/wakatime.d.ts.map +1 -1
  90. package/build/fetchers/wakatime.js +8 -4
  91. package/build/graphql/contributionsDocument.d.ts +18 -0
  92. package/build/graphql/contributionsDocument.d.ts.map +1 -0
  93. package/build/graphql/contributionsDocument.js +30 -0
  94. package/build/graphql/generated/common.d.ts +42 -0
  95. package/build/graphql/generated/common.d.ts.map +1 -0
  96. package/build/graphql/generated/common.js +2 -0
  97. package/build/graphql/generated/gist.d.ts +44 -0
  98. package/build/graphql/generated/gist.d.ts.map +1 -0
  99. package/build/graphql/generated/gist.js +27 -0
  100. package/build/graphql/generated/repo.d.ts +67 -0
  101. package/build/graphql/generated/repo.d.ts.map +1 -0
  102. package/build/graphql/generated/repo.js +30 -0
  103. package/build/graphql/generated/stats.d.ts +150 -0
  104. package/build/graphql/generated/stats.d.ts.map +1 -0
  105. package/build/graphql/generated/stats.js +95 -0
  106. package/build/graphql/generated/top-languages.d.ts +54 -0
  107. package/build/graphql/generated/top-languages.d.ts.map +1 -0
  108. package/build/graphql/generated/top-languages.js +27 -0
  109. package/build/graphql/graphqlDocument.d.ts +22 -0
  110. package/build/graphql/graphqlDocument.d.ts.map +1 -0
  111. package/build/graphql/graphqlDocument.js +8 -0
  112. package/build/graphql/reposContributedToDocument.d.ts +27 -0
  113. package/build/graphql/reposContributedToDocument.d.ts.map +1 -0
  114. package/build/graphql/reposContributedToDocument.js +59 -0
  115. package/build/index.d.ts +3 -1
  116. package/build/index.d.ts.map +1 -1
  117. package/build/index.js +2 -1
  118. package/build/themes/index.d.ts +32 -0
  119. package/build/themes/index.d.ts.map +1 -1
  120. package/build/themes/index.js +28 -0
  121. package/build/translations.d.ts +17 -0
  122. package/build/translations.d.ts.map +1 -1
  123. package/build/translations.js +17 -0
  124. package/package.json +14 -8
  125. package/src/_emoji-name-map.d.ts +10 -0
  126. package/src/_github-username-regex.d.ts +4 -0
  127. package/src/api/gist.js +31 -31
  128. package/src/api/index.js +40 -42
  129. package/src/api/pin.js +20 -38
  130. package/src/api/top-langs.js +36 -49
  131. package/src/api/wakatime.js +31 -31
  132. package/src/{calculateRank.js → calculateRank.ts} +29 -19
  133. package/src/cards/common-options.ts +13 -0
  134. package/src/cards/{gist.js → gist.ts} +37 -37
  135. package/src/cards/{repo.js → repo.ts} +107 -79
  136. package/src/cards/{stats.js → stats.ts} +173 -210
  137. package/src/cards/{top-languages.js → top-languages.ts} +325 -243
  138. package/src/cards/{wakatime.js → wakatime.ts} +158 -127
  139. package/src/common/Card.ts +123 -43
  140. package/src/common/I18n.ts +6 -2
  141. package/src/common/color.ts +240 -28
  142. package/src/common/config.ts +22 -29
  143. package/src/common/constants.ts +3 -1
  144. package/src/common/date.ts +45 -0
  145. package/src/common/html.ts +1 -1
  146. package/src/common/http.ts +71 -0
  147. package/src/common/icons.ts +1 -0
  148. package/src/common/languageColors.json +10 -1
  149. package/src/common/languageColors.ts +19 -0
  150. package/src/common/ops.ts +158 -0
  151. package/src/common/render.ts +652 -0
  152. package/src/common/retryer.ts +21 -12
  153. package/src/fetchers/gist.ts +79 -0
  154. package/src/fetchers/repo.ts +114 -0
  155. package/src/fetchers/stats.ts +743 -0
  156. package/src/fetchers/top-languages.ts +135 -0
  157. package/src/fetchers/{types.d.ts → types.ts} +28 -20
  158. package/src/fetchers/{wakatime.js → wakatime.ts} +19 -5
  159. package/src/graphql/contributionsDocument.ts +44 -0
  160. package/src/graphql/generated/common.ts +26 -0
  161. package/src/graphql/generated/gist.ts +59 -0
  162. package/src/graphql/generated/repo.ts +90 -0
  163. package/src/graphql/generated/stats.ts +194 -0
  164. package/src/graphql/generated/top-languages.ts +75 -0
  165. package/src/graphql/graphqlDocument.ts +25 -0
  166. package/src/graphql/queries/gist.graphql +25 -0
  167. package/src/graphql/queries/repo.graphql +28 -0
  168. package/src/graphql/queries/stats.graphql +120 -0
  169. package/src/graphql/queries/top-languages.graphql +34 -0
  170. package/src/graphql/reposContributedToDocument.ts +86 -0
  171. package/src/index.ts +3 -1
  172. package/src/themes/index.ts +33 -0
  173. package/src/translations.ts +17 -0
  174. package/src/cards/types.d.ts +0 -78
  175. package/src/common/http.js +0 -19
  176. package/src/common/ops.js +0 -170
  177. package/src/common/render.js +0 -440
  178. package/src/fetchers/gist.js +0 -112
  179. package/src/fetchers/repo.js +0 -161
  180. package/src/fetchers/stats.js +0 -474
  181. package/src/fetchers/top-languages.js +0 -175
  182. package/src/themes/README.md +0 -229
@@ -2,167 +2,106 @@ import axios from "axios";
2
2
  import githubUsernameRegex from "github-username-regex";
3
3
  import { calculateRank } from "../calculateRank.js";
4
4
  import { getConfig } from "../common/config.js";
5
+ import { getGitHubYearRange, toGitHubDateTime } from "../common/date.js";
5
6
  import { CustomError, MissingParamError } from "../common/error.js";
6
7
  import { wrapTextMultiline } from "../common/fmt.js";
7
- import { request } from "../common/http.js";
8
+ import { createGraphQLFetcher } from "../common/http.js";
8
9
  import { logger } from "../common/log.js";
9
10
  import { buildSearchFilter, parseOwnerAffiliations } from "../common/ops.js";
10
11
  import { retryer } from "../common/retryer.js";
11
- // GraphQL queries.
12
- const GRAPHQL_REPOS_FIELD = `
13
- repositories(first: 100, after: $after, ownerAffiliations: $ownerAffiliations, orderBy: {direction: DESC, field: STARGAZERS}) {
14
- totalCount
15
- nodes {
16
- name
17
- stargazerCount
18
- }
19
- pageInfo {
20
- hasNextPage
21
- endCursor
22
- }
23
- }
24
- `;
25
- const GRAPHQL_REPOS_QUERY = `
26
- query userInfo($login: String!, $after: String, $ownerAffiliations: [RepositoryAffiliation]) {
27
- user(login: $login) {
28
- ${GRAPHQL_REPOS_FIELD}
29
- }
30
- }
31
- `;
32
- const GRAPHQL_STATS_QUERY = `
33
- query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {
34
- user(login: $login) {
35
- name
36
- login
37
- commits: contributionsCollection (from: $startTime) {
38
- totalCommitContributions,
39
- }
40
- reviews: contributionsCollection {
41
- totalPullRequestReviewContributions
42
- }
43
- repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
44
- totalCount
45
- }
46
- pullRequests(first: 1) {
47
- totalCount
48
- }
49
- mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {
50
- totalCount
51
- }
52
- openIssues: issues(states: OPEN) {
53
- totalCount
54
- }
55
- closedIssues: issues(states: CLOSED) {
56
- totalCount
57
- }
58
- followers {
59
- totalCount
60
- }
61
- repositoryDiscussions @include(if: $includeDiscussions) {
62
- totalCount
63
- }
64
- repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {
65
- totalCount
66
- }
67
- ${GRAPHQL_REPOS_FIELD}
68
- }
69
- }
70
- `;
71
- /**
72
- * Stats fetcher object.
73
- *
74
- * @param {object & { after: string | null }} variables Fetcher variables.
75
- * @param {string} token GitHub token.
76
- * @returns {Promise<import('axios').AxiosResponse>} Axios response.
77
- */
78
- const fetcher = (variables, token) => {
79
- const query = variables.after ? GRAPHQL_REPOS_QUERY : GRAPHQL_STATS_QUERY;
80
- return request({
81
- query,
82
- variables,
83
- }, {
84
- Authorization: `bearer ${token}`,
85
- });
86
- };
12
+ import { buildContributionsDocument } from "../graphql/contributionsDocument.js";
13
+ import { UserInfoDocument, UserReposDocument, } from "../graphql/generated/stats.js";
14
+ import { MAX_REPOSITORIES_LIMIT, buildReposContributedToDocument, } from "../graphql/reposContributedToDocument.js";
15
+ const fetcher = createGraphQLFetcher(UserInfoDocument, "bearer");
16
+ /** Fetcher for the pages after the first, which only need `repositories`. */
17
+ const reposFetcher = createGraphQLFetcher(UserReposDocument, "bearer");
87
18
  /**
88
19
  * Fetch stats information for a given username.
89
20
  *
90
- * @param {object} variables Fetcher variables.
91
- * @param {string} variables.username GitHub username.
92
- * @param {boolean} variables.includeMergedPullRequests Include merged pull requests.
93
- * @param {boolean} variables.includeDiscussions Include discussions.
94
- * @param {boolean} variables.includeDiscussionsAnswers Include discussions answers.
95
- * @param {string|undefined} variables.startTime Time to start the count of total commits.
96
- * @param {string[]} variables.ownerAffiliations The owner affiliations to filter by. Default: OWNER.
97
- * @param {string | null} variables.pat PAT override or null.
98
- * @returns {Promise<import('axios').AxiosResponse>} Axios response.
21
+ * @param variables Fetcher variables.
22
+ * @param variables.username GitHub username.
23
+ * @param variables.includeMergedPullRequests Include merged pull requests.
24
+ * @param variables.includeDiscussions Include discussions.
25
+ * @param variables.includeDiscussionsAnswers Include discussions answers.
26
+ * @param variables.startTime Time to start the count of total commits.
27
+ * @param variables.ownerAffiliations The owner affiliations to filter by. Default: OWNER.
28
+ * @param variables.includeUserRepositories Whether to include the user's own repositories in the repos contributed to.
29
+ * @param variables.pat PAT override or null.
30
+ * @returns The stats response, with every fetched page's repos merged in.
99
31
  *
100
- * @description This function supports multi-page fetching if the 'FETCH_MULTI_PAGE_STARS' environment variable is set to true or a limit of fetches.
32
+ * @description Supports multi-page fetching when the `FETCH_MULTI_PAGE_STARS`
33
+ * env variable is `true` or a fetch limit.
101
34
  */
102
- const statsFetcher = async ({ username, includeMergedPullRequests, includeDiscussions, includeDiscussionsAnswers, startTime, ownerAffiliations, pat, }) => {
103
- let stats;
104
- let hasNextPage = true;
105
- let endCursor = null;
106
- let fetchedPages = 0;
107
- while (hasNextPage) {
108
- const variables = {
109
- login: username,
110
- first: 100,
111
- after: endCursor,
112
- includeMergedPullRequests,
113
- includeDiscussions,
114
- includeDiscussionsAnswers,
115
- startTime,
116
- ownerAffiliations,
117
- };
118
- let res = await retryer(fetcher, variables, pat);
119
- if (res.data.errors) {
120
- return res;
121
- }
122
- // Store stats data.
123
- const repoNodes = res.data.data.user.repositories.nodes;
124
- if (stats) {
125
- if (fetchedPages === 1) {
126
- // make deep copy of relevant stats fields to avoid altering the cached response object in frontend
127
- stats = structuredClone({
128
- data: stats.data,
129
- statusText: stats.statusText,
130
- });
131
- }
132
- stats.data.data.user.repositories.nodes.push(...repoNodes);
35
+ const statsFetcher = async ({ username, includeMergedPullRequests, includeDiscussions, includeDiscussionsAnswers, startTime, ownerAffiliations, includeUserRepositories, pat, }) => {
36
+ // only the first request carries the stats themselves
37
+ let stats = await retryer(fetcher, {
38
+ login: username,
39
+ after: null,
40
+ includeMergedPullRequests,
41
+ includeDiscussions,
42
+ includeDiscussionsAnswers,
43
+ startTime,
44
+ ownerAffiliations,
45
+ includeUserRepositories,
46
+ }, pat);
47
+ if (stats.data.errors) {
48
+ return stats;
49
+ }
50
+ const pageLimit = getConfig().fetchMultiPageStars;
51
+ const extraRepoNodes = [];
52
+ let pageRepositories = stats.data.data.user?.repositories;
53
+ let previousCursor = null;
54
+ let fetchedPages = 1;
55
+ while (fetchedPages < pageLimit &&
56
+ // an unstarred repo on the page means the starred ones are exhausted
57
+ !pageRepositories?.nodes?.some((node) => node?.stargazerCount === 0) &&
58
+ pageRepositories?.pageInfo.hasNextPage) {
59
+ const after = pageRepositories.pageInfo.endCursor;
60
+ // a null or non-advancing cursor would refetch the same page forever
61
+ if (after === null || after === previousCursor) {
62
+ break;
133
63
  }
134
- else {
135
- stats = res;
64
+ previousCursor = after;
65
+ const page = await retryer(reposFetcher, { login: username, after, ownerAffiliations }, pat);
66
+ if (page.data.errors) {
67
+ return {
68
+ data: { ...stats.data, errors: page.data.errors },
69
+ statusText: page.statusText,
70
+ };
136
71
  }
137
- // Disable multi page fetching on public Vercel instance due to rate limits.
72
+ pageRepositories = page.data.data.user?.repositories;
73
+ extraRepoNodes.push(...(pageRepositories?.nodes ?? []));
138
74
  fetchedPages++;
139
- const repoNodesWithStars = repoNodes.filter((node) => node.stargazerCount !== 0);
140
- hasNextPage =
141
- (getConfig().fetchMultiPageStars === "true" ||
142
- getConfig().fetchMultiPageStars > fetchedPages) &&
143
- repoNodes.length === repoNodesWithStars.length &&
144
- res.data.data.user.repositories.pageInfo.hasNextPage;
145
- endCursor = res.data.data.user.repositories.pageInfo.endCursor;
75
+ }
76
+ if (extraRepoNodes.length > 0) {
77
+ // deep copy to avoid mutating the response cached by the frontend
78
+ stats = structuredClone({
79
+ data: stats.data,
80
+ statusText: stats.statusText,
81
+ });
82
+ stats.data.data.user?.repositories.nodes?.push(...extraRepoNodes);
146
83
  }
147
84
  return stats;
148
85
  };
149
86
  /**
150
- * Fetch total commits using the REST API.
87
+ * Fetch total items count using the REST search API.
151
88
  *
152
- * @param {object} variables Fetcher variables.
153
- * @param {string} token GitHub token.
154
- * @returns {Promise<import('axios').AxiosResponse>} Axios response.
89
+ * @param variables Fetcher variables.
90
+ * @param token GitHub token.
91
+ * @returns Axios response.
155
92
  *
156
93
  * @see https://developer.github.com/v3/search/#search-commits
157
94
  */
158
95
  const fetchTotalItems = (variables, token) => {
96
+ const type = String(variables["type"]);
97
+ const filter = String(variables["filter"]);
98
+ const repo = variables["repo"];
99
+ const owner = variables["owner"];
159
100
  return axios({
160
101
  method: "get",
161
- url: `https://api.github.com/search/` +
162
- variables.type +
163
- `?per_page=1&q=` +
164
- buildSearchFilter(variables.repo, variables.owner).replaceAll(" ", "+") +
165
- variables.filter,
102
+ url: `https://api.github.com/search/${type}?per_page=1&q=` +
103
+ buildSearchFilter(repo, owner).replaceAll(" ", "+") +
104
+ filter,
166
105
  headers: {
167
106
  "Content-Type": "application/json",
168
107
  Accept: "application/vnd.github.cloak-preview",
@@ -171,13 +110,14 @@ const fetchTotalItems = (variables, token) => {
171
110
  });
172
111
  };
173
112
  /**
174
- * Fetch all the commits for all the repositories of a given username.
113
+ * Fetch a total count for a given username via the REST search API.
175
114
  *
176
- * @param {string} username GitHub username.
177
- * @returns {Promise<number>} Total commits.
115
+ * @param username GitHub username.
116
+ * @returns Total count.
178
117
  *
179
- * @description Done like this because the GitHub API does not provide a way to fetch all the commits. See
180
- * #92#issuecomment-661026467 and #211 for more information.
118
+ * The GraphQL API can't return this.
119
+ * @see https://github.com/anuraghazra/github-readme-stats/issues/92#issuecomment-661026467
120
+ * @see https://github.com/anuraghazra/github-readme-stats/pull/211
181
121
  */
182
122
  const totalItemsFetcher = async (username, repo, owner, type, filter, pat) => {
183
123
  if (!githubUsernameRegex.test(username)) {
@@ -186,27 +126,21 @@ const totalItemsFetcher = async (username, repo, owner, type, filter, pat) => {
186
126
  }
187
127
  let res;
188
128
  try {
189
- res = await retryer(fetchTotalItems, {
190
- login: username,
191
- repo,
192
- owner,
193
- type,
194
- filter,
195
- }, pat);
129
+ res = await retryer(fetchTotalItems, { login: username, repo, owner, type, filter }, pat);
196
130
  }
197
131
  catch (err) {
198
132
  logger.log(err);
199
133
  throw err;
200
134
  }
201
135
  const totalCount = res.data.total_count;
202
- if (isNaN(totalCount)) {
136
+ if (totalCount === undefined || isNaN(totalCount)) {
203
137
  logger.error("GitHub error: " + JSON.stringify(res.data));
204
138
  throw new CustomError("Could not fetch data from GitHub REST API.", CustomError.GITHUB_REST_API_ERROR);
205
139
  }
206
140
  return totalCount;
207
141
  };
208
142
  const fetchRepoUserStats = async (username, repo, owner, include_prs_authored, include_prs_commented, include_prs_reviewed, include_issues_authored, include_issues_commented, pat) => {
209
- let stats = {};
143
+ const stats = {};
210
144
  if (include_prs_authored) {
211
145
  stats.totalPRsAuthored = await totalItemsFetcher(username, repo, owner, "issues", `author:${username}+type:pr`, pat);
212
146
  }
@@ -224,20 +158,192 @@ const fetchRepoUserStats = async (username, repo, owner, include_prs_authored, i
224
158
  }
225
159
  return stats;
226
160
  };
161
+ /**
162
+ * Turn a GraphQL `errors` payload into the error to throw.
163
+ *
164
+ * @param errors Errors from the response envelope.
165
+ * @param statusText HTTP status text, used as the error type when GitHub gave a message.
166
+ * @param fallback Message when GitHub gave none.
167
+ */
168
+ const graphqlError = (errors, statusText, fallback) => {
169
+ logger.error(errors);
170
+ const message = errors[0]?.message;
171
+ return message
172
+ ? new CustomError(wrapTextMultiline(message, 525, 12)[0] ?? "", statusText)
173
+ : new CustomError(fallback, CustomError.GRAPHQL_ERROR);
174
+ };
175
+ /**
176
+ * Fetch all-time contributions by building a single GraphQL query
177
+ * for all the given years.
178
+ *
179
+ * Whether private contributions are included depends on the user's profile settings:
180
+ * https://docs.github.com/en/account-and-profile/how-tos/contribution-settings/manage-visibility-settings-for-private-contributions-and-achievements#changing-the-visibility-of-your-private-contributions
181
+ */
182
+ const fetchTotalContributions = async (username, years, pat = null) => {
183
+ if (years.length === 0) {
184
+ return 0;
185
+ }
186
+ const contributionsFetcher = createGraphQLFetcher(buildContributionsDocument(years), "bearer");
187
+ const contribRes = await retryer(contributionsFetcher, { login: username }, pat);
188
+ if (contribRes.data.errors) {
189
+ throw graphqlError(contribRes.data.errors, contribRes.statusText, "Something went wrong while trying to retrieve the contributions data using the GraphQL API.");
190
+ }
191
+ const user = contribRes.data.data.user;
192
+ if (!user) {
193
+ return 0;
194
+ }
195
+ let total = 0;
196
+ for (const year of years) {
197
+ const yearBlock = user[`year_${year}`];
198
+ if (yearBlock?.contributionCalendar.totalContributions) {
199
+ total += yearBlock.contributionCalendar.totalContributions;
200
+ }
201
+ }
202
+ return total;
203
+ };
204
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
205
+ /**
206
+ * Ranges per request.
207
+ * Each costs roughly `4 * MAX_REPOSITORIES_LIMIT` nodes,
208
+ * so an unchunked request for a heavily split account would breach GitHub's 500k node ceiling.
209
+ * GitHub can still reject a request of this size, in which case the chunk size is halved
210
+ * and the request retried; it is increased back up to this maximum after every success.
211
+ */
212
+ const MAX_RANGES_PER_REQUEST = 400;
213
+ const REPOS_CONTRIBUTED_TO_ERROR = "Something went wrong while trying to retrieve the repository contributions data using the GraphQL API.";
214
+ /** GraphQL error type GitHub returns when a query exceeds its node/complexity budget. */
215
+ const RESOURCE_LIMITS_EXCEEDED = "RESOURCE_LIMITS_EXCEEDED";
216
+ /**
217
+ * Whether GitHub rejected the query for returning too many results.
218
+ *
219
+ * @see https://docs.github.com/en/graphql/overview/rate-limits-and-query-limits-for-the-graphql-api#other-resource-limits
220
+ *
221
+ * @param errors Errors from the response envelope.
222
+ * @returns True if the query has to be retried with fewer ranges.
223
+ */
224
+ const isResourceLimitsExceeded = (errors) => errors.some((error) => error.type === RESOURCE_LIMITS_EXCEEDED);
225
+ /**
226
+ * Count the repositories a user contributed to across every contribution year.
227
+ *
228
+ * `repositoriesContributedTo` spans at most one year,
229
+ * so every year is fetched as an aliased `contributionsCollection(from, to)` in one request and the repos de-duplicated.
230
+ * Ranges are worked off a queue, multiple ranges at a time.
231
+ * A range returning `MAX_REPOSITORIES_LIMIT` results may have more,
232
+ * so it is halved and both halves are queued again.
233
+ * When GitHub rejects a request with `RESOURCE_LIMITS_EXCEEDED` or a timeout,
234
+ * it is retried with half as many ranges.
235
+ * When GitHub rejects a request with an empty response, it is retried up to 3 times.
236
+ *
237
+ * Whether private contributions are included depends on the used PAT.
238
+ *
239
+ * @param canonicalUsername Username as GitHub reports it; `nameWithOwner` uses its
240
+ * casing, which the raw query-string username need not match.
241
+ * @param years Contribution years to walk.
242
+ * @param includeOwnRepos Whether to count the user's own repositories.
243
+ * @param pat Optional PAT override.
244
+ * @returns Count of repositories.
245
+ */
246
+ const fetchAllTimeReposContributedTo = async (canonicalUsername, years, includeOwnRepos, pat) => {
247
+ const repos = new Set();
248
+ const pending = years.map(getGitHubYearRange);
249
+ // halved when GitHub rejects a request for exceeding its resource limits, increases on success
250
+ let rangesPerRequest = MAX_RANGES_PER_REQUEST;
251
+ let emptyResponseRetries = 0;
252
+ const MAX_EMPTY_RESPONSE_RETRIES = 3;
253
+ while (pending.length > 0) {
254
+ const chunk = pending.slice(0, rangesPerRequest);
255
+ const fetcher = createGraphQLFetcher(buildReposContributedToDocument(chunk, includeOwnRepos), "bearer");
256
+ const res = await retryer(fetcher, { login: canonicalUsername, maxRepositories: MAX_REPOSITORIES_LIMIT }, pat);
257
+ if (res.status === 502 || res.status === 504) {
258
+ if (chunk.length > 1) {
259
+ rangesPerRequest = Math.floor(chunk.length / 2);
260
+ logger.log(`Gateway timeout (${res.status}), retrying with ${rangesPerRequest} range(s) per request...`);
261
+ continue;
262
+ }
263
+ throw new CustomError(REPOS_CONTRIBUTED_TO_ERROR, CustomError.GRAPHQL_ERROR);
264
+ }
265
+ if (res.data.errors) {
266
+ if (isResourceLimitsExceeded(res.data.errors) && chunk.length > 1) {
267
+ rangesPerRequest = Math.floor(chunk.length / 2);
268
+ logger.log(`Resource limits exceeded, retrying with ${rangesPerRequest} range(s) per request...`);
269
+ continue;
270
+ }
271
+ throw graphqlError(res.data.errors, res.statusText, REPOS_CONTRIBUTED_TO_ERROR);
272
+ }
273
+ if (typeof res.data !== "object") {
274
+ if (emptyResponseRetries < MAX_EMPTY_RESPONSE_RETRIES) {
275
+ emptyResponseRetries++;
276
+ logger.log(`Empty response from GitHub, retrying (${emptyResponseRetries}/${MAX_EMPTY_RESPONSE_RETRIES})...`);
277
+ continue;
278
+ }
279
+ throw new CustomError(REPOS_CONTRIBUTED_TO_ERROR, CustomError.GRAPHQL_ERROR);
280
+ }
281
+ emptyResponseRetries = 0;
282
+ const user = res.data.data.user;
283
+ if (!user) {
284
+ throw new CustomError(REPOS_CONTRIBUTED_TO_ERROR, CustomError.GRAPHQL_ERROR);
285
+ }
286
+ pending.splice(0, chunk.length);
287
+ rangesPerRequest = Math.min(MAX_RANGES_PER_REQUEST, Math.ceil(rangesPerRequest * 1.25));
288
+ for (const [index, range] of chunk.entries()) {
289
+ const rangeResponse = user[`range_${index}`];
290
+ if (!rangeResponse) {
291
+ throw new CustomError(REPOS_CONTRIBUTED_TO_ERROR, CustomError.GRAPHQL_ERROR);
292
+ }
293
+ const lists = [
294
+ rangeResponse.commitContributionsByRepository,
295
+ rangeResponse.issueContributionsByRepository,
296
+ rangeResponse.pullRequestContributionsByRepository,
297
+ (rangeResponse.repositoryContributions?.nodes ?? []).filter((node) => node !== null),
298
+ ];
299
+ const isSaturated = lists.some((list) => list.length >= MAX_REPOSITORIES_LIMIT);
300
+ const rangeDays = Math.round((range.to.getTime() - range.from.getTime()) / MS_PER_DAY);
301
+ if (isSaturated && rangeDays >= 2) {
302
+ const mid = new Date(range.from.getTime() + Math.floor(rangeDays / 2) * MS_PER_DAY);
303
+ logger.log(`Range ${range.from.toISOString()} - ${range.to.toISOString()} is saturated, splitting and retrying...`);
304
+ // GitHub only reads the date portion,
305
+ // so the first half ends 1 second before `mid` to keep the halves from sharing a day
306
+ pending.push({ from: range.from, to: new Date(mid.getTime() - 1000) }, { from: mid, to: range.to });
307
+ continue;
308
+ }
309
+ if (isSaturated) {
310
+ logger.log(`Range ${range.from.toISOString()} - ${range.to.toISOString()} is saturated but cannot be split further.`);
311
+ }
312
+ for (const { repository } of lists.flat()) {
313
+ const name = repository.nameWithOwner;
314
+ if (includeOwnRepos || !name.startsWith(`${canonicalUsername}/`)) {
315
+ repos.add(name);
316
+ }
317
+ }
318
+ }
319
+ }
320
+ return repos.size;
321
+ };
227
322
  /**
228
323
  * Fetch stats for a given username.
229
324
  *
230
- * @param {string} username GitHub username.
231
- * @param {boolean} include_all_commits Include all commits.
232
- * @param {string[]} exclude_repo Repositories to exclude.
233
- * @param {boolean} include_merged_pull_requests Include merged pull requests.
234
- * @param {boolean} include_discussions Include discussions.
235
- * @param {boolean} include_discussions_answers Include discussions answers.
236
- * @param {number|undefined} commits_year Year to count total commits
237
- * @param {string[]} ownerAffiliations Owner affiliations. Default: OWNER.
238
- * @returns {Promise<import("./types").StatsData>} Stats data.
325
+ * @param username GitHub username.
326
+ * @param include_all_commits Include all commits.
327
+ * @param exclude_repo Repositories to exclude.
328
+ * @param include_merged_pull_requests Include merged pull requests.
329
+ * @param include_discussions Include discussions.
330
+ * @param include_discussions_answers Include discussions answers.
331
+ * @param commits_year Year to count total commits.
332
+ * @param repo Repositories to scope the REST search to.
333
+ * @param owner Owners to scope the REST search to.
334
+ * @param include_prs_authored Include count of PRs authored.
335
+ * @param include_prs_commented Include count of PRs commented.
336
+ * @param include_prs_reviewed Include count of PRs reviewed.
337
+ * @param include_issues_authored Include count of issues authored.
338
+ * @param include_issues_commented Include count of issues commented.
339
+ * @param ownerAffiliations Owner affiliations. Default: OWNER.
340
+ * @param include_contributions Include all-time contributions.
341
+ * @param include_all_time_contribs Include all-time count of repos contributed to.
342
+ * @param contribs_include_own_repos Include user-owned repos in contributed-to counts.
343
+ * @param pat Optional PAT override.
344
+ * @returns Stats data.
239
345
  */
240
- const fetchStats = async (username, include_all_commits = false, exclude_repo = [], include_merged_pull_requests = false, include_discussions = false, include_discussions_answers = false, commits_year, repo = [], owner = [], include_prs_authored = false, include_prs_commented = false, include_prs_reviewed = false, include_issues_authored = false, include_issues_commented = false, ownerAffiliations = [], pat = null) => {
346
+ const fetchStats = async (username, include_all_commits = false, exclude_repo = [], include_merged_pull_requests = false, include_discussions = false, include_discussions_answers = false, commits_year, repo = [], owner = [], include_prs_authored = false, include_prs_commented = false, include_prs_reviewed = false, include_issues_authored = false, include_issues_commented = false, ownerAffiliations = [], include_contributions = false, include_all_time_contribs = false, contribs_include_own_repos = false, pat = null) => {
241
347
  if (!username) {
242
348
  throw new MissingParamError(["username"]);
243
349
  }
@@ -253,34 +359,44 @@ const fetchStats = async (username, include_all_commits = false, exclude_repo =
253
359
  totalDiscussionsStarted: 0,
254
360
  totalDiscussionsAnswered: 0,
255
361
  contributedTo: 0,
362
+ allTimeContributedTo: 0,
256
363
  totalPRsAuthored: 0,
257
364
  totalPRsCommented: 0,
258
365
  totalPRsReviewed: 0,
259
366
  totalIssuesAuthored: 0,
260
367
  totalIssuesCommented: 0,
368
+ totalContributions: 0,
261
369
  rank: { level: "C", percentile: 100 },
262
370
  };
263
- ownerAffiliations = parseOwnerAffiliations(ownerAffiliations);
264
- let res = await statsFetcher({
371
+ const affiliations = parseOwnerAffiliations(ownerAffiliations);
372
+ const res = await statsFetcher({
265
373
  username,
266
374
  includeMergedPullRequests: include_merged_pull_requests,
267
375
  includeDiscussions: include_discussions,
268
376
  includeDiscussionsAnswers: include_discussions_answers,
269
- startTime: commits_year ? `${commits_year}-01-01T00:00:00Z` : undefined,
270
- ownerAffiliations,
271
- }, pat);
377
+ startTime: commits_year === undefined
378
+ ? undefined
379
+ : toGitHubDateTime(getGitHubYearRange(commits_year).from),
380
+ ownerAffiliations: affiliations,
381
+ includeUserRepositories: contribs_include_own_repos,
382
+ pat,
383
+ });
272
384
  // Catch GraphQL errors.
273
385
  if (res.data.errors) {
274
386
  logger.error(res.data.errors);
275
- if (res.data.errors[0].type === "NOT_FOUND") {
276
- throw new CustomError(res.data.errors[0].message || "Could not fetch user.", CustomError.USER_NOT_FOUND);
387
+ const firstError = res.data.errors[0];
388
+ if (firstError?.type === "NOT_FOUND") {
389
+ throw new CustomError(firstError.message || "Could not fetch user.", CustomError.USER_NOT_FOUND);
277
390
  }
278
- if (res.data.errors[0].message) {
279
- throw new CustomError(wrapTextMultiline(res.data.errors[0].message, 525, 12)[0], res.statusText);
391
+ if (firstError?.message) {
392
+ throw new CustomError(wrapTextMultiline(firstError.message, 525, 12)[0] ?? "", res.statusText);
280
393
  }
281
394
  throw new CustomError("Something went wrong while trying to retrieve the stats data using the GraphQL API.", CustomError.GRAPHQL_ERROR);
282
395
  }
283
396
  const user = res.data.data.user;
397
+ if (!user) {
398
+ throw new CustomError("Could not fetch user.", CustomError.USER_NOT_FOUND);
399
+ }
284
400
  stats.name = user.name || user.login;
285
401
  // if include_all_commits, fetch all commits using the REST API.
286
402
  if (include_all_commits) {
@@ -289,38 +405,40 @@ const fetchStats = async (username, include_all_commits = false, exclude_repo =
289
405
  else {
290
406
  stats.totalCommits = user.commits.totalCommitContributions;
291
407
  }
292
- let repoUserStats = await fetchRepoUserStats(username, repo, owner, include_prs_authored, include_prs_commented, include_prs_reviewed, include_issues_authored, include_issues_commented, pat);
408
+ const repoUserStats = await fetchRepoUserStats(username, repo, owner, include_prs_authored, include_prs_commented, include_prs_reviewed, include_issues_authored, include_issues_commented, pat);
293
409
  Object.assign(stats, repoUserStats);
294
410
  stats.totalPRs = user.pullRequests.totalCount;
295
411
  if (include_merged_pull_requests) {
296
- stats.totalPRsMerged = user.mergedPullRequests.totalCount;
412
+ const mergedCount = user.mergedPullRequests?.totalCount ?? 0;
413
+ stats.totalPRsMerged = mergedCount;
297
414
  stats.mergedPRsPercentage =
298
- (user.mergedPullRequests.totalCount / user.pullRequests.totalCount) *
299
- 100 || 0;
415
+ (mergedCount / user.pullRequests.totalCount) * 100 || 0;
300
416
  }
301
417
  stats.totalReviews = user.reviews.totalPullRequestReviewContributions;
302
418
  stats.totalIssues = user.openIssues.totalCount + user.closedIssues.totalCount;
303
419
  if (include_discussions) {
304
- stats.totalDiscussionsStarted = user.repositoryDiscussions.totalCount;
420
+ stats.totalDiscussionsStarted = user.repositoryDiscussions?.totalCount ?? 0;
305
421
  }
306
422
  if (include_discussions_answers) {
307
423
  stats.totalDiscussionsAnswered =
308
- user.repositoryDiscussionComments.totalCount;
424
+ user.repositoryDiscussionComments?.totalCount ?? 0;
309
425
  }
310
426
  stats.contributedTo = user.repositoriesContributedTo.totalCount;
427
+ if (include_contributions) {
428
+ stats.totalContributions = await fetchTotalContributions(username, user.contributionsCollection.contributionYears, pat);
429
+ }
430
+ if (include_all_time_contribs) {
431
+ stats.allTimeContributedTo = await fetchAllTimeReposContributedTo(user.login, user.contributionsCollection.contributionYears, contribs_include_own_repos, pat);
432
+ }
311
433
  // Retrieve stars while filtering out repositories to be hidden.
312
434
  const allExcludedRepos = [
313
435
  ...exclude_repo,
314
436
  ...getConfig().excludeRepositories,
315
437
  ];
316
- let repoToHide = new Set(allExcludedRepos);
317
- stats.totalStars = user.repositories.nodes
318
- .filter((data) => {
319
- return !repoToHide.has(data.name);
320
- })
321
- .reduce((prev, curr) => {
322
- return prev + curr.stargazerCount;
323
- }, 0);
438
+ const repoToHide = new Set(allExcludedRepos);
439
+ stats.totalStars = (user.repositories.nodes ?? [])
440
+ .filter((data) => !!data && !repoToHide.has(data.name))
441
+ .reduce((prev, curr) => prev + (curr?.stargazerCount ?? 0), 0);
324
442
  stats.rank = calculateRank({
325
443
  all_commits: include_all_commits,
326
444
  commits: stats.totalCommits,
@@ -1,20 +1,15 @@
1
- /**
2
- * Top languages data.
3
- */
4
- export type TopLangData = any;
5
- /**
6
- * @typedef {import("./types").TopLangData} TopLangData Top languages data.
7
- */
1
+ import type { TopLangData } from "./types.js";
8
2
  /**
9
3
  * Fetch top languages for a given username.
10
4
  *
11
- * @param {string} username GitHub username.
12
- * @param {string[]} exclude_repo List of repositories to exclude. Default: [].
13
- * @param {number} size_weight Weightage to be given to size.
14
- * @param {number} count_weight Weightage to be given to count.
15
- * @param {string[]} ownerAffiliations The owner affiliations to filter by. Default: OWNER.
16
- * @param {string|null} pat Optional PAT override.
17
- * @returns {Promise<TopLangData>} Top languages data.
5
+ * @param username GitHub username.
6
+ * @param exclude_repo List of repositories to exclude. Default: [].
7
+ * @param size_weight Weightage to be given to size.
8
+ * @param count_weight Weightage to be given to count.
9
+ * @param ownerAffiliations The owner affiliations to filter by. Default: OWNER.
10
+ * @param pat Optional PAT override.
11
+ * @returns Top languages data.
18
12
  */
19
- export function fetchTopLanguages(username: string, exclude_repo?: string[], size_weight?: number, count_weight?: number, ownerAffiliations?: string[], pat?: string | null): Promise<TopLangData>;
13
+ declare const fetchTopLanguages: (username: string, exclude_repo?: Array<string>, size_weight?: number, count_weight?: number, ownerAffiliations?: Array<string>, pat?: string | null) => Promise<TopLangData>;
14
+ export { fetchTopLanguages };
20
15
  //# sourceMappingURL=top-languages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"top-languages.d.ts","sourceRoot":"","sources":["../../src/fetchers/top-languages.js"],"names":[],"mappings":";;;;AA+CA;;GAEG;AAEH;;;;;;;;;;GAUG;AACH,4CARW,MAAM,iBACN,MAAM,EAAE,gBACR,MAAM,iBACN,MAAM,sBACN,MAAM,EAAE,QACR,MAAM,GAAC,IAAI,GACT,OAAO,CAAC,WAAW,CAAC,CAgHhC"}
1
+ {"version":3,"file":"top-languages.d.ts","sourceRoot":"","sources":["../../src/fetchers/top-languages.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAQ,WAAW,EAAE,MAAM,YAAY,CAAC;AAIpD;;;;;;;;;;GAUG;AACH,QAAA,MAAM,iBAAiB,GACrB,UAAU,MAAM,EAChB,eAAc,KAAK,CAAC,MAAM,CAAM,EAChC,oBAAe,EACf,qBAAgB,EAChB,oBAAmB,KAAK,CAAC,MAAM,CAAM,EACrC,MAAK,MAAM,GAAG,IAAW,KACxB,OAAO,CAAC,WAAW,CAiGrB,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,CAAC"}