@stats-forge/github-stats-forge-core 0.1.0 → 0.3.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 (142) hide show
  1. package/build/api/contributed-to.d.ts +23 -0
  2. package/build/api/contributed-to.d.ts.map +1 -0
  3. package/build/api/contributed-to.js +43 -0
  4. package/build/api/gist.d.ts +8 -15
  5. package/build/api/gist.d.ts.map +1 -1
  6. package/build/api/gist.js +14 -29
  7. package/build/api/handler.d.ts +16 -0
  8. package/build/api/handler.d.ts.map +1 -0
  9. package/build/api/handler.js +29 -0
  10. package/build/api/index.d.ts +1 -0
  11. package/build/api/index.d.ts.map +1 -1
  12. package/build/api/index.js +1 -0
  13. package/build/api/params.d.ts +11 -5
  14. package/build/api/params.d.ts.map +1 -1
  15. package/build/api/params.js +26 -6
  16. package/build/api/pin.d.ts +11 -15
  17. package/build/api/pin.d.ts.map +1 -1
  18. package/build/api/pin.js +34 -46
  19. package/build/api/stats.d.ts +15 -22
  20. package/build/api/stats.d.ts.map +1 -1
  21. package/build/api/stats.js +59 -72
  22. package/build/api/top-langs.d.ts +11 -22
  23. package/build/api/top-langs.d.ts.map +1 -1
  24. package/build/api/top-langs.js +34 -51
  25. package/build/api/wakatime.d.ts +10 -24
  26. package/build/api/wakatime.d.ts.map +1 -1
  27. package/build/api/wakatime.js +27 -47
  28. package/build/cards/contributed-to.d.ts +19 -0
  29. package/build/cards/contributed-to.d.ts.map +1 -0
  30. package/build/cards/contributed-to.js +242 -0
  31. package/build/cards/gist.d.ts +1 -0
  32. package/build/cards/gist.d.ts.map +1 -1
  33. package/build/cards/gist.js +13 -9
  34. package/build/cards/repo.d.ts +12 -1
  35. package/build/cards/repo.d.ts.map +1 -1
  36. package/build/cards/repo.js +40 -18
  37. package/build/cards/stats.d.ts +15 -3
  38. package/build/cards/stats.d.ts.map +1 -1
  39. package/build/cards/stats.js +91 -76
  40. package/build/cards/top-languages.d.ts +13 -2
  41. package/build/cards/top-languages.d.ts.map +1 -1
  42. package/build/cards/top-languages.js +26 -9
  43. package/build/cards/wakatime.d.ts +13 -2
  44. package/build/cards/wakatime.d.ts.map +1 -1
  45. package/build/cards/wakatime.js +22 -9
  46. package/build/common/Card.d.ts +9 -0
  47. package/build/common/Card.d.ts.map +1 -1
  48. package/build/common/Card.js +44 -9
  49. package/build/common/I18n.d.ts +1 -1
  50. package/build/common/I18n.d.ts.map +1 -1
  51. package/build/common/I18n.js +4 -2
  52. package/build/common/brand.d.ts +76 -0
  53. package/build/common/brand.d.ts.map +1 -0
  54. package/build/common/brand.js +73 -0
  55. package/build/common/constants.d.ts +3 -1
  56. package/build/common/constants.d.ts.map +1 -1
  57. package/build/common/constants.js +3 -1
  58. package/build/common/date.d.ts +50 -3
  59. package/build/common/date.d.ts.map +1 -1
  60. package/build/common/date.js +104 -2
  61. package/build/common/icons.d.ts +4 -2
  62. package/build/common/icons.d.ts.map +1 -1
  63. package/build/common/icons.js +10 -5
  64. package/build/common/ops.d.ts +8 -3
  65. package/build/common/ops.d.ts.map +1 -1
  66. package/build/common/ops.js +8 -14
  67. package/build/common/render.d.ts +7 -1
  68. package/build/common/render.d.ts.map +1 -1
  69. package/build/common/render.js +69 -61
  70. package/build/fetchers/contributed-to.d.ts +62 -0
  71. package/build/fetchers/contributed-to.d.ts.map +1 -0
  72. package/build/fetchers/contributed-to.js +184 -0
  73. package/build/fetchers/graphql-error.d.ts +13 -0
  74. package/build/fetchers/graphql-error.d.ts.map +1 -0
  75. package/build/fetchers/graphql-error.js +29 -0
  76. package/build/fetchers/index.d.ts +2 -1
  77. package/build/fetchers/index.d.ts.map +1 -1
  78. package/build/fetchers/index.js +1 -0
  79. package/build/fetchers/stats.d.ts +14 -8
  80. package/build/fetchers/stats.d.ts.map +1 -1
  81. package/build/fetchers/stats.js +72 -195
  82. package/build/fetchers/top-languages.d.ts.map +1 -1
  83. package/build/fetchers/top-languages.js +3 -18
  84. package/build/fetchers/types.d.ts +24 -0
  85. package/build/fetchers/types.d.ts.map +1 -1
  86. package/build/graphql/contributionsCollection.d.ts +9 -0
  87. package/build/graphql/contributionsCollection.d.ts.map +1 -0
  88. package/build/graphql/contributionsCollection.js +12 -0
  89. package/build/graphql/contributionsDocument.d.ts +5 -5
  90. package/build/graphql/contributionsDocument.d.ts.map +1 -1
  91. package/build/graphql/contributionsDocument.js +8 -15
  92. package/build/graphql/generated/contributed-to.d.ts +22 -0
  93. package/build/graphql/generated/contributed-to.d.ts.map +1 -0
  94. package/build/graphql/generated/contributed-to.js +11 -0
  95. package/build/graphql/generated/stats.d.ts +13 -1
  96. package/build/graphql/generated/stats.d.ts.map +1 -1
  97. package/build/graphql/generated/stats.js +2 -2
  98. package/build/graphql/graphqlDocument.d.ts +1 -1
  99. package/build/graphql/reposContributedToDocument.d.ts.map +1 -1
  100. package/build/graphql/reposContributedToDocument.js +11 -5
  101. package/build/translations.d.ts +64 -1
  102. package/build/translations.d.ts.map +1 -1
  103. package/build/translations.js +65 -1
  104. package/package.json +1 -1
  105. package/src/api/contributed-to.ts +84 -0
  106. package/src/api/gist.ts +20 -37
  107. package/src/api/handler.ts +42 -0
  108. package/src/api/index.ts +1 -0
  109. package/src/api/params.ts +48 -9
  110. package/src/api/pin.ts +33 -45
  111. package/src/api/stats.ts +78 -89
  112. package/src/api/top-langs.ts +34 -56
  113. package/src/api/wakatime.ts +33 -54
  114. package/src/cards/contributed-to.ts +373 -0
  115. package/src/cards/gist.ts +17 -8
  116. package/src/cards/repo.ts +44 -20
  117. package/src/cards/stats.ts +119 -90
  118. package/src/cards/top-languages.ts +27 -18
  119. package/src/cards/wakatime.ts +23 -13
  120. package/src/common/Card.ts +56 -11
  121. package/src/common/I18n.ts +4 -2
  122. package/src/common/brand.ts +109 -0
  123. package/src/common/constants.ts +4 -1
  124. package/src/common/date.ts +135 -3
  125. package/src/common/icons.ts +10 -5
  126. package/src/common/ops.ts +10 -19
  127. package/src/common/render.ts +75 -65
  128. package/src/fetchers/contributed-to.ts +271 -0
  129. package/src/fetchers/graphql-error.ts +36 -0
  130. package/src/fetchers/index.ts +3 -0
  131. package/src/fetchers/stats.ts +123 -274
  132. package/src/fetchers/top-languages.ts +5 -19
  133. package/src/fetchers/types.ts +26 -0
  134. package/src/graphql/contributionsCollection.ts +18 -0
  135. package/src/graphql/contributionsDocument.ts +11 -18
  136. package/src/graphql/generated/contributed-to.ts +25 -0
  137. package/src/graphql/generated/stats.ts +19 -6
  138. package/src/graphql/graphqlDocument.ts +1 -1
  139. package/src/graphql/queries/contributed-to.graphql +8 -0
  140. package/src/graphql/queries/stats.graphql +14 -2
  141. package/src/graphql/reposContributedToDocument.ts +11 -9
  142. package/src/translations.ts +77 -0
@@ -1,15 +1,15 @@
1
1
  import { calculateRank } from '../calculateRank.js';
2
2
  import { GITHUB_USERNAME_PATTERN } from '../common/constants.js';
3
- import { getGitHubYearRange, toGitHubDateTime } from '../common/date.js';
3
+ import { getWidestRange, toContributionRanges, toGitHubDateTime, toRange, toYearRanges, } from '../common/date.js';
4
4
  import { CardError, USER_NOT_FOUND } from '../common/error.js';
5
5
  import { createGraphQLFetcher, httpRequest } from '../common/http.js';
6
6
  import { logger } from '../common/log.js';
7
- import { buildSearchFilter, chunkArray, parseOwnerAffiliations } from '../common/ops.js';
8
- import { wrapTextMultiline } from '../common/render.js';
7
+ import { buildSearchFilter, parseOwnerAffiliations } from '../common/ops.js';
9
8
  import { retryer } from '../common/retryer.js';
10
9
  import { buildContributionsDocument } from '../graphql/contributionsDocument.js';
11
10
  import { UserInfoDocument, UserReposDocument } from '../graphql/generated/stats.js';
12
- import { MAX_REPOSITORIES_LIMIT, buildReposContributedToDocument, } from '../graphql/reposContributedToDocument.js';
11
+ import { fetchReposContributedTo } from './contributed-to.js';
12
+ import { graphqlError } from './graphql-error.js';
13
13
  const fetcher = createGraphQLFetcher(UserInfoDocument, 'bearer');
14
14
  /** Fetcher for the pages after the first, which only need `repositories`. */
15
15
  const reposFetcher = createGraphQLFetcher(UserReposDocument, 'bearer');
@@ -21,7 +21,7 @@ const reposFetcher = createGraphQLFetcher(UserReposDocument, 'bearer');
21
21
  *
22
22
  * @returns The stats response, with every fetched page's repos merged in.
23
23
  */
24
- const statsFetcher = async ({ username, includeMergedPullRequests, includeDiscussions, includeDiscussionsAnswers, startTime, ownerAffiliations, includeUserRepositories, }, config) => {
24
+ const statsFetcher = async ({ username, includeMergedPullRequests, includeDiscussions, includeDiscussionsAnswers, commitsRange, ownerAffiliations, includeUserRepositories, }, config) => {
25
25
  // only the first request carries the stats themselves
26
26
  let stats = await retryer(fetcher, {
27
27
  login: username,
@@ -29,7 +29,9 @@ const statsFetcher = async ({ username, includeMergedPullRequests, includeDiscus
29
29
  includeMergedPullRequests,
30
30
  includeDiscussions,
31
31
  includeDiscussionsAnswers,
32
- startTime,
32
+ // both ends together: an open `to` would run the range into the next 1st of January
33
+ startTime: commitsRange && toGitHubDateTime(commitsRange.from),
34
+ endTime: commitsRange && toGitHubDateTime(commitsRange.to),
33
35
  ownerAffiliations,
34
36
  includeUserRepositories,
35
37
  }, config);
@@ -79,20 +81,14 @@ const statsFetcher = async ({ username, includeMergedPullRequests, includeDiscus
79
81
  *
80
82
  * @returns The search response, carrying `total_count`.
81
83
  */
82
- const fetchTotalItems = (variables, token, { fetch }) => {
83
- const type = String(variables['type']);
84
- const filter = String(variables['filter']);
85
- const repo = variables['repo'];
86
- const owner = variables['owner'];
87
- return httpRequest(fetch, `https://api.github.com/search/${type}?per_page=1&q=${buildSearchFilter(repo, owner).replaceAll(' ', '+')}${filter}`, {
88
- method: 'GET',
89
- headers: {
90
- 'Content-Type': 'application/json',
91
- Accept: 'application/vnd.github.cloak-preview',
92
- Authorization: `token ${token}`,
93
- },
94
- });
95
- };
84
+ const fetchTotalItems = ({ type, filter, repo, owner }, token, { fetch }) => httpRequest(fetch, `https://api.github.com/search/${type}?per_page=1&q=${buildSearchFilter(repo, owner).replaceAll(' ', '+')}${filter}`, {
85
+ method: 'GET',
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ Accept: 'application/vnd.github.cloak-preview',
89
+ Authorization: `token ${token}`,
90
+ },
91
+ });
96
92
  /**
97
93
  * Fetch a total count for a given username via the REST search API.
98
94
  *
@@ -102,7 +98,7 @@ const fetchTotalItems = (variables, token, { fetch }) => {
102
98
  *
103
99
  * @returns Total count.
104
100
  */
105
- const totalItemsFetcher = async ({ username, repo, owner, type, filter, }, config) => {
101
+ const totalItemsFetcher = async ({ username, ...search }, config) => {
106
102
  if (!GITHUB_USERNAME_PATTERN.test(username)) {
107
103
  logger.log('Invalid username provided.');
108
104
  throw new CardError('Invalid username provided.', {
@@ -112,7 +108,7 @@ const totalItemsFetcher = async ({ username, repo, owner, type, filter, }, confi
112
108
  }
113
109
  let res;
114
110
  try {
115
- res = await retryer(fetchTotalItems, { login: username, repo, owner, type, filter }, config);
111
+ res = await retryer(fetchTotalItems, search, config);
116
112
  }
117
113
  catch (error) {
118
114
  logger.log(error);
@@ -127,85 +123,55 @@ const totalItemsFetcher = async ({ username, repo, owner, type, filter, }, confi
127
123
  }
128
124
  return totalCount;
129
125
  };
126
+ /** Each count the REST search API answers: the flag asking for it, and the qualifiers that find it. */
127
+ const REPO_USER_SEARCHES = [
128
+ ['include_prs_authored', 'totalPRsAuthored', (username) => `author:${username}+type:pr`],
129
+ [
130
+ 'include_prs_commented',
131
+ 'totalPRsCommented',
132
+ (username) => `commenter:${username}+-author:${username}+type:pr`,
133
+ ],
134
+ [
135
+ 'include_prs_reviewed',
136
+ 'totalPRsReviewed',
137
+ (username) => `reviewed-by:${username}+-author:${username}+type:pr`,
138
+ ],
139
+ ['include_issues_authored', 'totalIssuesAuthored', (username) => `author:${username}+type:issue`],
140
+ [
141
+ 'include_issues_commented',
142
+ 'totalIssuesCommented',
143
+ (username) => `commenter:${username}+-author:${username}+type:issue`,
144
+ ],
145
+ ];
130
146
  /**
131
147
  * Fetch the per-repository counts the REST search API answers, one request each.
132
148
  *
133
149
  * @returns Only the counts that were asked for.
134
150
  */
135
- const fetchRepoUserStats = async ({ username, repo = [], owner = [], include_prs_authored = false, include_prs_commented = false, include_prs_reviewed = false, include_issues_authored = false, include_issues_commented = false, }, config) => {
151
+ const fetchRepoUserStats = async ({ username, repo = [], owner = [], ...flags }, config) => {
136
152
  const stats = {};
137
- if (include_prs_authored) {
138
- stats.totalPRsAuthored = await totalItemsFetcher({
139
- username,
140
- repo,
141
- owner,
142
- type: 'issues',
143
- filter: `author:${username}+type:pr`,
144
- }, config);
145
- }
146
- if (include_prs_commented) {
147
- stats.totalPRsCommented = await totalItemsFetcher({
148
- username,
149
- repo,
150
- owner,
151
- type: 'issues',
152
- filter: `commenter:${username}+-author:${username}+type:pr`,
153
- }, config);
154
- }
155
- if (include_prs_reviewed) {
156
- stats.totalPRsReviewed = await totalItemsFetcher({
157
- username,
158
- repo,
159
- owner,
160
- type: 'issues',
161
- filter: `reviewed-by:${username}+-author:${username}+type:pr`,
162
- }, config);
163
- }
164
- if (include_issues_authored) {
165
- stats.totalIssuesAuthored = await totalItemsFetcher({
166
- username,
167
- repo,
168
- owner,
169
- type: 'issues',
170
- filter: `author:${username}+type:issue`,
171
- }, config);
172
- }
173
- if (include_issues_commented) {
174
- stats.totalIssuesCommented = await totalItemsFetcher({
175
- username,
176
- repo,
177
- owner,
178
- type: 'issues',
179
- filter: `commenter:${username}+-author:${username}+type:issue`,
180
- }, config);
153
+ for (const [flag, stat, filter] of REPO_USER_SEARCHES) {
154
+ if (flags[flag]) {
155
+ stats[stat] = await totalItemsFetcher({ username, repo, owner, type: 'issues', filter: filter(username) }, config);
156
+ }
181
157
  }
182
158
  return stats;
183
159
  };
184
160
  /**
185
- * Turn a GraphQL `errors` payload into the error to throw.
186
- */
187
- const graphqlError = (errors, statusText, fallback) => {
188
- logger.error(errors);
189
- const message = errors[0]?.message;
190
- return message
191
- ? new CardError(wrapTextMultiline(message, 525, 12)[0] ?? '', {
192
- code: 'upstream',
193
- secondaryMessage: statusText,
194
- })
195
- : new CardError(fallback, { code: 'upstream' });
196
- };
197
- /**
198
- * Fetch all-time contributions by building a single GraphQL query
199
- * for all the given years.
161
+ * Sum one figure over a set of ranges, fetched as aliased fields of a single query.
200
162
  *
201
163
  * Whether private contributions are included depends on the user's profile settings:
202
164
  * 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
165
+ *
166
+ * @returns The total over every range, and 0 when there are none.
203
167
  */
204
- const fetchTotalContributions = async (username, years, config) => {
205
- if (years.length === 0) {
168
+ const sumOverRanges = async (username, ranges,
169
+ /** What one range contributes to the total. */
170
+ pick, config) => {
171
+ if (ranges.length === 0) {
206
172
  return 0;
207
173
  }
208
- const contributionsFetcher = createGraphQLFetcher(buildContributionsDocument(years), 'bearer');
174
+ const contributionsFetcher = createGraphQLFetcher(buildContributionsDocument(ranges), 'bearer');
209
175
  const contribRes = await retryer(contributionsFetcher, { login: username }, config);
210
176
  if (contribRes.data.errors) {
211
177
  throw graphqlError(contribRes.data.errors, contribRes.statusText, 'Something went wrong while trying to retrieve the contributions data using the GraphQL API.');
@@ -215,102 +181,17 @@ const fetchTotalContributions = async (username, years, config) => {
215
181
  return 0;
216
182
  }
217
183
  let total = 0;
218
- for (const year of years) {
219
- const yearBlock = user[`year_${year}`];
220
- if (yearBlock?.contributionCalendar.totalContributions) {
221
- total += yearBlock.contributionCalendar.totalContributions;
222
- }
184
+ for (const block of Object.values(user)) {
185
+ total += pick(block);
223
186
  }
224
187
  return total;
225
188
  };
226
- const MS_PER_DAY = 24 * 60 * 60 * 1000;
227
- /**
228
- * Ranges per request.
229
- * Each costs roughly `4 * MAX_REPOSITORIES_LIMIT` nodes,
230
- * so an unchunked round of a heavily split account would breach GitHub's 500k node ceiling
231
- * and lose the ranges that had already resolved along with it.
232
- */
233
- const MAX_RANGES_PER_REQUEST = 100;
234
- const REPOS_CONTRIBUTED_TO_ERROR = 'Something went wrong while trying to retrieve the repository contributions data using the GraphQL API.';
235
- /**
236
- * Count the repositories a user contributed to across every contribution year.
237
- *
238
- * `repositoriesContributedTo` spans at most one year,
239
- * so every year is fetched as an aliased `contributionsCollection(from, to)` in one request and the repos de-duplicated.
240
- * A range returning `MAX_REPOSITORIES_LIMIT` results may have more,
241
- * so it is halved and requeried in the next round.
242
- *
243
- * Whether private contributions are included depends on the used PAT.
244
- *
245
- * @returns Count of repositories.
246
- */
247
- const fetchAllTimeReposContributedTo = async (
248
- /** `nameWithOwner` uses GitHub's casing, which the query-string username need not match */
249
- canonicalUsername, years, includeOwnRepos, config) => {
250
- const repos = new Set();
251
- let pending = years.map((year) => getGitHubYearRange(year));
252
- while (pending.length > 0) {
253
- const nextPending = [];
254
- for (const chunk of chunkArray(pending, MAX_RANGES_PER_REQUEST)) {
255
- const chunkFetcher = createGraphQLFetcher(buildReposContributedToDocument(chunk, includeOwnRepos), 'bearer');
256
- const res = await retryer(chunkFetcher, { login: canonicalUsername, maxRepositories: MAX_REPOSITORIES_LIMIT }, config);
257
- if (res.data.errors) {
258
- throw graphqlError(res.data.errors, res.statusText, REPOS_CONTRIBUTED_TO_ERROR);
259
- }
260
- const { user } = res.data.data;
261
- if (!user) {
262
- throw new CardError(REPOS_CONTRIBUTED_TO_ERROR, { code: 'upstream' });
263
- }
264
- for (const [index, range] of chunk.entries()) {
265
- const rangeResponse = user[`range_${index}`];
266
- if (!rangeResponse) {
267
- throw new CardError(REPOS_CONTRIBUTED_TO_ERROR, {
268
- code: 'upstream',
269
- });
270
- }
271
- const lists = [
272
- rangeResponse.commitContributionsByRepository,
273
- rangeResponse.issueContributionsByRepository,
274
- rangeResponse.pullRequestContributionsByRepository,
275
- (rangeResponse.repositoryContributions?.nodes ?? []).filter((node) => node !== null),
276
- ];
277
- const isSaturated = lists.some((list) => list.length >= MAX_REPOSITORIES_LIMIT);
278
- const rangeDays = Math.round((range.to.getTime() - range.from.getTime()) / MS_PER_DAY);
279
- // a range of 1 day or less can't be split any further
280
- if (isSaturated && rangeDays >= 2) {
281
- // every `from` sits on UTC midnight, so the split lands on a day boundary too
282
- const mid = new Date(range.from.getTime() + Math.floor(rangeDays / 2) * MS_PER_DAY);
283
- // GitHub only reads the date portion,
284
- // so the first half ends 1 second before `mid` to keep the halves from sharing a day
285
- nextPending.push({ from: range.from, to: new Date(mid.getTime() - 1000) }, { from: mid, to: range.to });
286
- continue;
287
- }
288
- if (isSaturated) {
289
- logger.log(`Range ${range.from.toISOString()} - ${range.to.toISOString()} is saturated but cannot be split further.`);
290
- }
291
- for (const { repository } of lists.flat()) {
292
- const name = repository.nameWithOwner;
293
- if (includeOwnRepos || !name.startsWith(`${canonicalUsername}/`)) {
294
- repos.add(name);
295
- }
296
- }
297
- }
298
- }
299
- // each saturated range pushes both of its halves
300
- const saturatedCount = nextPending.length / 2;
301
- if (saturatedCount > 0) {
302
- logger.log(`found ${saturatedCount} saturated ranges, splitting and retrying...`);
303
- }
304
- pending = nextPending;
305
- }
306
- return repos.size;
307
- };
308
189
  /**
309
190
  * Fetch stats for a given username.
310
191
  *
311
192
  * @returns Stats data.
312
193
  */
313
- 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, }, config) => {
194
+ const fetchStats = async ({ username, include_all_commits = false, exclude_repo = [], include_merged_pull_requests = false, include_discussions = false, include_discussions_answers = false, from, to, 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, }, config) => {
314
195
  if (!username) {
315
196
  throw CardError.missingParam(['username']);
316
197
  }
@@ -321,6 +202,7 @@ const fetchStats = async ({ username, include_all_commits = false, exclude_repo
321
202
  mergedPRsPercentage: 0,
322
203
  totalReviews: 0,
323
204
  totalCommits: 0,
205
+ commitsRange: undefined,
324
206
  totalIssues: 0,
325
207
  totalStars: 0,
326
208
  totalDiscussionsStarted: 0,
@@ -336,34 +218,22 @@ const fetchStats = async ({ username, include_all_commits = false, exclude_repo
336
218
  rank: { level: 'C', percentile: 100 },
337
219
  };
338
220
  const affiliations = parseOwnerAffiliations(ownerAffiliations);
221
+ // neither end given leaves GitHub's trailing year in place, which `userInfo` already carries
222
+ const commitsRange = from === undefined && to === undefined ? undefined : toRange(from, to);
223
+ const commitsYearRanges = commitsRange ? toYearRanges(commitsRange) : [];
224
+ // one calendar year is a range `userInfo` can ask for itself; anything else is summed separately
225
+ const inlineRange = commitsYearRanges.length === 1 ? commitsYearRanges[0] : undefined;
339
226
  const res = await statsFetcher({
340
227
  username,
341
228
  includeMergedPullRequests: include_merged_pull_requests,
342
229
  includeDiscussions: include_discussions,
343
230
  includeDiscussionsAnswers: include_discussions_answers,
344
- startTime: commits_year === undefined
345
- ? undefined
346
- : toGitHubDateTime(getGitHubYearRange(commits_year).from),
231
+ commitsRange: inlineRange,
347
232
  ownerAffiliations: affiliations,
348
233
  includeUserRepositories: contribs_include_own_repos,
349
234
  }, config);
350
- // Catch GraphQL errors.
351
235
  if (res.data.errors) {
352
- logger.error(res.data.errors);
353
- const [firstError] = res.data.errors;
354
- if (firstError?.type === 'NOT_FOUND') {
355
- throw new CardError(firstError.message || 'Could not fetch user.', {
356
- code: 'not_found',
357
- secondaryMessage: USER_NOT_FOUND,
358
- });
359
- }
360
- if (firstError?.message) {
361
- throw new CardError(wrapTextMultiline(firstError.message, 525, 12)[0] ?? '', {
362
- code: 'upstream',
363
- secondaryMessage: res.statusText,
364
- });
365
- }
366
- throw new CardError('Something went wrong while trying to retrieve the stats data using the GraphQL API.', { code: 'upstream' });
236
+ throw graphqlError(res.data.errors, res.statusText, 'Something went wrong while trying to retrieve the stats data using the GraphQL API.');
367
237
  }
368
238
  const { user } = res.data.data;
369
239
  if (!user) {
@@ -384,7 +254,12 @@ const fetchStats = async ({ username, include_all_commits = false, exclude_repo
384
254
  }, config);
385
255
  }
386
256
  else {
387
- stats.totalCommits = user.commits.totalCommitContributions;
257
+ stats.commitsRange = commitsRange;
258
+ // an inverted range sums no years, which is no commits rather than GitHub's default year
259
+ stats.totalCommits =
260
+ commitsRange && !inlineRange
261
+ ? await sumOverRanges(username, commitsYearRanges, (block) => block.totalCommitContributions, config)
262
+ : user.commits.totalCommitContributions;
388
263
  }
389
264
  const repoUserStats = await fetchRepoUserStats({
390
265
  username,
@@ -412,11 +287,13 @@ const fetchStats = async ({ username, include_all_commits = false, exclude_repo
412
287
  stats.totalDiscussionsAnswered = user.repositoryDiscussionComments?.totalCount ?? 0;
413
288
  }
414
289
  stats.contributedTo = user.repositoriesContributedTo.totalCount;
290
+ const contributionRanges = toContributionRanges(user.contributionsCollection.contributionYears, getWidestRange());
415
291
  if (include_contributions) {
416
- stats.totalContributions = await fetchTotalContributions(username, user.contributionsCollection.contributionYears, config);
292
+ stats.totalContributions = await sumOverRanges(username, contributionRanges, (block) => block.contributionCalendar.totalContributions, config);
417
293
  }
418
294
  if (include_all_time_contribs) {
419
- stats.allTimeContributedTo = await fetchAllTimeReposContributedTo(user.login, user.contributionsCollection.contributionYears, contribs_include_own_repos, config);
295
+ const reposContributedTo = await fetchReposContributedTo(user.login, contributionRanges, contribs_include_own_repos, config);
296
+ stats.allTimeContributedTo = reposContributedTo.size;
420
297
  }
421
298
  // Retrieve stars while filtering out repositories to be hidden.
422
299
  const allExcludedRepos = [...exclude_repo, ...config.excludeRepositories];
@@ -1 +1 @@
1
- {"version":3,"file":"top-languages.d.ts","sourceRoot":"","sources":["../../src/fetchers/top-languages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAatD,OAAO,KAAK,EAAQ,WAAW,EAAE,MAAM,YAAY,CAAC;AAIpD;;;;GAIG;AACH,QAAA,MAAM,iBAAiB,8EAOlB;IACD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,iBAAiB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACnC,UACO,UAAU,KACjB,OAAO,CAAC,WAAW,CAgFrB,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
1
+ {"version":3,"file":"top-languages.d.ts","sourceRoot":"","sources":["../../src/fetchers/top-languages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAYtD,OAAO,KAAK,EAAQ,WAAW,EAAE,MAAM,YAAY,CAAC;AAIpD;;;;GAIG;AACH,QAAA,MAAM,iBAAiB,8EAOlB;IACD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,iBAAiB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACnC,UACO,UAAU,KACjB,OAAO,CAAC,WAAW,CAmErB,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
@@ -1,10 +1,9 @@
1
- import { CardError, USER_NOT_FOUND } from '../common/error.js';
1
+ import { CardError } from '../common/error.js';
2
2
  import { createGraphQLFetcher } from '../common/http.js';
3
- import { logger } from '../common/log.js';
4
3
  import { parseOwnerAffiliations } from '../common/ops.js';
5
- import { wrapTextMultiline } from '../common/render.js';
6
4
  import { retryer } from '../common/retryer.js';
7
5
  import { TopLanguagesDocument } from '../graphql/generated/top-languages.js';
6
+ import { graphqlError } from './graphql-error.js';
8
7
  const fetcher = createGraphQLFetcher(TopLanguagesDocument, 'token');
9
8
  /**
10
9
  * Fetch top languages for a given username.
@@ -21,21 +20,7 @@ const fetchTopLanguages = async ({ username, exclude_repo = [], size_weight = 1,
21
20
  ownerAffiliations: affiliations,
22
21
  }, config);
23
22
  if (res.data.errors) {
24
- logger.error(res.data.errors);
25
- const [firstError] = res.data.errors;
26
- if (firstError?.type === 'NOT_FOUND') {
27
- throw new CardError(firstError.message || 'Could not fetch user.', {
28
- code: 'not_found',
29
- secondaryMessage: USER_NOT_FOUND,
30
- });
31
- }
32
- if (firstError?.message) {
33
- throw new CardError(wrapTextMultiline(firstError.message, 525, 12)[0] ?? '', {
34
- code: 'upstream',
35
- secondaryMessage: res.statusText,
36
- });
37
- }
38
- throw new CardError('Something went wrong while trying to retrieve the language data using the GraphQL API.', { code: 'upstream' });
23
+ throw graphqlError(res.data.errors, res.statusText, 'Something went wrong while trying to retrieve the language data using the GraphQL API.');
39
24
  }
40
25
  const repoToHide = {};
41
26
  const allExcludedRepos = [...exclude_repo, ...config.excludeRepositories];
@@ -1,3 +1,4 @@
1
+ import type { GitHubDateRange } from '../common/date.ts';
1
2
  import type { RepoInfoFragment } from '../graphql/generated/repo.ts';
2
3
  export interface GistData {
3
4
  name: string;
@@ -33,6 +34,8 @@ export interface StatsData {
33
34
  mergedPRsPercentage: number;
34
35
  totalReviews: number;
35
36
  totalCommits: number;
37
+ /** The range `totalCommits` covers; `undefined` when it is GitHub's own last year, or all time. */
38
+ commitsRange: GitHubDateRange | undefined;
36
39
  totalIssues: number;
37
40
  totalStars: number;
38
41
  totalDiscussionsStarted: number;
@@ -50,6 +53,27 @@ export interface StatsData {
50
53
  percentile: number;
51
54
  };
52
55
  }
56
+ /** One repository a user contributed to, as the card ranks it. */
57
+ export interface ContributedRepo {
58
+ nameWithOwner: string;
59
+ /**
60
+ * Commit-days plus issues and pull requests opened, summed over every year.
61
+ * Not a commit count: GitHub groups commit contributions by day, not by commit.
62
+ */
63
+ contributions: number;
64
+ /** Every year the user contributed to it, ascending. */
65
+ years: Array<number>;
66
+ }
67
+ export interface ContributedToData {
68
+ /** GitHub's own casing of the login, which the query param need not match. */
69
+ login: string;
70
+ /** The ranked slice, most contributions first. */
71
+ repos: Array<ContributedRepo>;
72
+ /** Every repository the walk found, which `repos` is a slice of. */
73
+ totalRepos: number;
74
+ /** The account's contribution years, ascending — the range the year marks cover. */
75
+ years: Array<number>;
76
+ }
53
77
  export interface Lang {
54
78
  name: string;
55
79
  color: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fetchers/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAErE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,KAAK,QAAQ,GAAG,IAAI,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,GAAG;IAC1D,eAAe,EAAE;QACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KACrB,GAAG,IAAI,CAAC;CACV,CAAC;AAEF,MAAM,WAAW,aAAa;IAE5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,aAAa,CAAC;AAEtD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,uBAAuB,EAAE,MAAM,CAAC;IAChC,wBAAwB,EAAE,MAAM,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IAEb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,KAAK,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,sCAAsC,EAAE,MAAM,CAAC;IAC/C,uBAAuB,EAAE,MAAM,CAAC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,KAAK,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B,EAAE,MAAM,CAAC;IACrC,qDAAqD,EAAE,MAAM,CAAC;IAC9D,oBAAoB,EAAE,MAAM,CAAC;IAC7B,6CAA6C,EAAE,MAAM,CAAC;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,mBAAmB,EAAE,OAAO,CAAC;IAC7B,0BAA0B,EAAE,OAAO,CAAC;IACpC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,sBAAsB,EAAE,OAAO,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,KAAK,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;IACH,iBAAiB,EAAE,KAAK,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,sCAAsC,EAAE,MAAM,CAAC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fetchers/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAErE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,KAAK,QAAQ,GAAG,IAAI,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,GAAG;IAC1D,eAAe,EAAE;QACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KACrB,GAAG,IAAI,CAAC;CACV,CAAC;AAEF,MAAM,WAAW,aAAa;IAE5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,aAAa,CAAC;AAEtD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,mGAAmG;IACnG,YAAY,EAAE,eAAe,GAAG,SAAS,CAAC;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,uBAAuB,EAAE,MAAM,CAAC;IAChC,wBAAwB,EAAE,MAAM,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED,kEAAkE;AAClE,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,KAAK,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IAC9B,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IAEb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,KAAK,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,sCAAsC,EAAE,MAAM,CAAC;IAC/C,uBAAuB,EAAE,MAAM,CAAC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,KAAK,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B,EAAE,MAAM,CAAC;IACrC,qDAAqD,EAAE,MAAM,CAAC;IAC9D,oBAAoB,EAAE,MAAM,CAAC;IAC7B,6CAA6C,EAAE,MAAM,CAAC;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,mBAAmB,EAAE,OAAO,CAAC;IAC7B,0BAA0B,EAAE,OAAO,CAAC;IACpC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,sBAAsB,EAAE,OAAO,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,KAAK,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;IACH,iBAAiB,EAAE,KAAK,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC,CAAC;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,sCAAsC,EAAE,MAAM,CAAC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB"}
@@ -0,0 +1,9 @@
1
+ import type { GitHubDateRange } from '../common/date.ts';
2
+ /**
3
+ * One `contributionsCollection` field per range, aliased by position.
4
+ *
5
+ * @returns The fields, for a query that selects `fragment`.
6
+ */
7
+ declare const aliasedRanges: (ranges: Array<GitHubDateRange>, fragment: string) => string;
8
+ export { aliasedRanges };
9
+ //# sourceMappingURL=contributionsCollection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contributionsCollection.d.ts","sourceRoot":"","sources":["../../src/graphql/contributionsCollection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAOzD;;;;GAIG;AACH,QAAA,MAAM,aAAa,WAAY,KAAK,CAAC,eAAe,CAAC,YAAY,MAAM,KAAG,MAG3D,CAAC;AAEhB,OAAO,EAAE,aAAa,EAAE,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { toGitHubDateTime } from '../common/date.js';
2
+ /** Naming `to` is not optional: GitHub defaults an omitted one to a year after `from`. */
3
+ const contributionsCollectionOf = ({ from, to }) => `contributionsCollection(from: "${toGitHubDateTime(from)}", to: "${toGitHubDateTime(to)}")`;
4
+ /**
5
+ * One `contributionsCollection` field per range, aliased by position.
6
+ *
7
+ * @returns The fields, for a query that selects `fragment`.
8
+ */
9
+ const aliasedRanges = (ranges, fragment) => ranges
10
+ .map((range, index) => `range_${index}: ${contributionsCollectionOf(range)} { ...${fragment} }`)
11
+ .join('\n');
12
+ export { aliasedRanges };
@@ -1,18 +1,18 @@
1
- import type { YearContributionsFragment } from './generated/stats.ts';
1
+ import type { GitHubDateRange } from '../common/date.ts';
2
+ import type { RangeContributionsFragment } from './generated/stats.ts';
2
3
  import type { GraphQLDocument } from './graphqlDocument.ts';
3
4
  interface ContributionsQueryVariables {
4
5
  login: string;
5
6
  }
6
7
  interface ContributionsQuery {
7
- user: Record<`year_${number}`, YearContributionsFragment> | null;
8
+ user: Record<`range_${number}`, RangeContributionsFragment> | null;
8
9
  }
9
10
  /**
10
- * Build the all-time contributions query, one aliased `contributionsCollection` field per year.
11
- * The shape is only known at runtime.
11
+ * Build the contributions query for a set of ranges. The shape is only known at runtime.
12
12
  *
13
13
  * @returns Document for `createGraphQLFetcher`.
14
14
  */
15
- declare const buildContributionsDocument: (years: Array<number>) => GraphQLDocument<ContributionsQuery, ContributionsQueryVariables>;
15
+ declare const buildContributionsDocument: (ranges: Array<GitHubDateRange>) => GraphQLDocument<ContributionsQuery, ContributionsQueryVariables>;
16
16
  export { buildContributionsDocument };
17
17
  export type { ContributionsQuery };
18
18
  //# sourceMappingURL=contributionsDocument.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contributionsDocument.d.ts","sourceRoot":"","sources":["../../src/graphql/contributionsDocument.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,UAAU,2BAA2B;IACnC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC,QAAQ,MAAM,EAAE,EAAE,yBAAyB,CAAC,GAAG,IAAI,CAAC;CAClE;AAED;;;;;GAKG;AACH,QAAA,MAAM,0BAA0B,UACvB,KAAK,CAAC,MAAM,CAAC,KACnB,eAAe,CAAC,kBAAkB,EAAE,2BAA2B,CAoBjE,CAAC;AAEF,OAAO,EAAE,0BAA0B,EAAE,CAAC;AACtC,YAAY,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"contributionsDocument.d.ts","sourceRoot":"","sources":["../../src/graphql/contributionsDocument.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AACvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,UAAU,2BAA2B;IACnC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC,SAAS,MAAM,EAAE,EAAE,0BAA0B,CAAC,GAAG,IAAI,CAAC;CACpE;AAED;;;;GAIG;AACH,QAAA,MAAM,0BAA0B,WACtB,KAAK,CAAC,eAAe,CAAC,KAC7B,eAAe,CAAC,kBAAkB,EAAE,2BAA2B,CAa/D,CAAC;AAEJ,OAAO,EAAE,0BAA0B,EAAE,CAAC;AACtC,YAAY,EAAE,kBAAkB,EAAE,CAAC"}
@@ -1,29 +1,22 @@
1
- import { getGitHubYearRange, toGitHubDateTime } from '../common/date.js';
1
+ import { aliasedRanges } from './contributionsCollection.js';
2
2
  import { graphqlDocument } from './graphqlDocument.js';
3
3
  /**
4
- * Build the all-time contributions query, one aliased `contributionsCollection` field per year.
5
- * The shape is only known at runtime.
4
+ * Build the contributions query for a set of ranges. The shape is only known at runtime.
6
5
  *
7
6
  * @returns Document for `createGraphQLFetcher`.
8
7
  */
9
- const buildContributionsDocument = (years) => {
10
- const yearFields = years
11
- .map((year) => {
12
- const { from, to } = getGitHubYearRange(year);
13
- return `year_${year}: contributionsCollection(from: "${toGitHubDateTime(from)}", to: "${toGitHubDateTime(to)}") { ...YearContributions }`;
14
- })
15
- .join('\n');
16
- // fragment must match queries/stats.graphql, which generates its type
17
- return graphqlDocument(`
8
+ const buildContributionsDocument = (ranges) =>
9
+ // fragment must match queries/stats.graphql, which generates its type
10
+ graphqlDocument(`
18
11
  query userContributions($login: String!) {
19
12
  user(login: $login) {
20
- ${yearFields}
13
+ ${aliasedRanges(ranges, 'RangeContributions')}
21
14
  }
22
15
  }
23
- fragment YearContributions on ContributionsCollection {
16
+ fragment RangeContributions on ContributionsCollection {
17
+ totalCommitContributions
24
18
  contributionCalendar {
25
19
  totalContributions
26
20
  }
27
21
  }`);
28
- };
29
22
  export { buildContributionsDocument };
@@ -0,0 +1,22 @@
1
+ /** Internal type. DO NOT USE DIRECTLY. */
2
+ type Exact<T extends {
3
+ [key: string]: unknown;
4
+ }> = {
5
+ [K in keyof T]: T[K];
6
+ };
7
+ export type UserContributionYearsQueryVariables = Exact<{
8
+ login: string;
9
+ }>;
10
+ export type UserContributionYearsQuery = {
11
+ user: {
12
+ login: string;
13
+ contributionsCollection: {
14
+ contributionYears: Array<number>;
15
+ };
16
+ } | null;
17
+ };
18
+ export declare const UserContributionYearsDocument: import("../graphqlDocument.ts").GraphQLDocument<UserContributionYearsQuery, Exact<{
19
+ login: string;
20
+ }>>;
21
+ export {};
22
+ //# sourceMappingURL=contributed-to.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contributed-to.d.ts","sourceRoot":"","sources":["../../../src/graphql/generated/contributed-to.ts"],"names":[],"mappings":"AAEA,0CAA0C;AAC1C,KAAK,KAAK,CAAC,CAAC,SAAS;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAE5E,MAAM,MAAM,mCAAmC,GAAG,KAAK,CAAC;IACtD,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,MAAM,MAAM,0BAA0B,GAAG;IACvC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,uBAAuB,EAAE;YAAE,iBAAiB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;SAAE,CAAA;KAAE,GAAG,IAAI,CAAC;CAC/F,CAAC;AAEF,eAAO,MAAM,6BAA6B;WAPjC,MAAM;GAkBZ,CAAC"}
@@ -0,0 +1,11 @@
1
+ // Generated file — see .github/CONTRIBUTING.md
2
+ import { graphqlDocument } from '../graphqlDocument.js';
3
+ export const UserContributionYearsDocument = graphqlDocument(`
4
+ query userContributionYears($login: String!) {
5
+ user(login: $login) {
6
+ login
7
+ contributionsCollection {
8
+ contributionYears
9
+ }
10
+ }
11
+ }`);