@stats-organization/github-readme-stats-core 2.1.5 → 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 (169) 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 +9 -41
  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 +23 -45
  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 +9 -41
  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 +11 -55
  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 +9 -41
  16. package/build/cards/common-options.d.ts +13 -0
  17. package/build/cards/common-options.d.ts.map +1 -0
  18. package/build/cards/common-options.js +1 -0
  19. package/build/cards/gist.d.ts +11 -16
  20. package/build/cards/gist.d.ts.map +1 -1
  21. package/build/cards/gist.js +26 -31
  22. package/build/cards/repo.d.ts +19 -16
  23. package/build/cards/repo.d.ts.map +1 -1
  24. package/build/cards/repo.js +58 -60
  25. package/build/cards/stats.d.ts +29 -46
  26. package/build/cards/stats.d.ts.map +1 -1
  27. package/build/cards/stats.js +113 -181
  28. package/build/cards/top-languages.d.ts +81 -77
  29. package/build/cards/top-languages.d.ts.map +1 -1
  30. package/build/cards/top-languages.js +174 -200
  31. package/build/cards/wakatime.d.ts +21 -11
  32. package/build/cards/wakatime.d.ts.map +1 -1
  33. package/build/cards/wakatime.js +73 -111
  34. package/build/common/Card.d.ts +34 -13
  35. package/build/common/Card.d.ts.map +1 -1
  36. package/build/common/Card.js +73 -40
  37. package/build/common/I18n.d.ts +4 -2
  38. package/build/common/I18n.d.ts.map +1 -1
  39. package/build/common/color.d.ts +60 -18
  40. package/build/common/color.d.ts.map +1 -1
  41. package/build/common/color.js +102 -5
  42. package/build/common/config.d.ts +2 -1
  43. package/build/common/config.d.ts.map +1 -1
  44. package/build/common/config.js +16 -26
  45. package/build/common/constants.d.ts +2 -1
  46. package/build/common/constants.d.ts.map +1 -1
  47. package/build/common/date.d.ts +37 -0
  48. package/build/common/date.d.ts.map +1 -0
  49. package/build/common/date.js +31 -0
  50. package/build/common/http.d.ts +25 -2
  51. package/build/common/http.d.ts.map +1 -1
  52. package/build/common/http.js +26 -3
  53. package/build/common/icons.d.ts +1 -0
  54. package/build/common/icons.d.ts.map +1 -1
  55. package/build/common/icons.js +1 -0
  56. package/build/common/languageColors.d.ts +11 -0
  57. package/build/common/languageColors.d.ts.map +1 -0
  58. package/build/common/languageColors.js +15 -0
  59. package/build/common/languageColors.json +1 -0
  60. package/build/common/ops.d.ts +3 -10
  61. package/build/common/ops.d.ts.map +1 -1
  62. package/build/common/ops.js +3 -15
  63. package/build/common/render.d.ts +43 -6
  64. package/build/common/render.d.ts.map +1 -1
  65. package/build/common/render.js +77 -11
  66. package/build/common/retryer.d.ts +3 -2
  67. package/build/common/retryer.d.ts.map +1 -1
  68. package/build/common/retryer.js +1 -0
  69. package/build/fetchers/gist.d.ts.map +1 -1
  70. package/build/fetchers/gist.js +8 -39
  71. package/build/fetchers/repo.d.ts +12 -11
  72. package/build/fetchers/repo.d.ts.map +1 -1
  73. package/build/fetchers/repo.js +26 -67
  74. package/build/fetchers/stats.d.ts +24 -17
  75. package/build/fetchers/stats.d.ts.map +1 -1
  76. package/build/fetchers/stats.js +302 -183
  77. package/build/fetchers/top-languages.d.ts +10 -15
  78. package/build/fetchers/top-languages.d.ts.map +1 -1
  79. package/build/fetchers/top-languages.js +52 -103
  80. package/build/fetchers/types.d.ts +24 -20
  81. package/build/fetchers/types.d.ts.map +1 -1
  82. package/build/fetchers/wakatime.d.ts +9 -5
  83. package/build/fetchers/wakatime.d.ts.map +1 -1
  84. package/build/fetchers/wakatime.js +8 -4
  85. package/build/graphql/contributionsDocument.d.ts +18 -0
  86. package/build/graphql/contributionsDocument.d.ts.map +1 -0
  87. package/build/graphql/contributionsDocument.js +30 -0
  88. package/build/graphql/generated/common.d.ts +42 -0
  89. package/build/graphql/generated/common.d.ts.map +1 -0
  90. package/build/graphql/generated/common.js +2 -0
  91. package/build/graphql/generated/gist.d.ts +44 -0
  92. package/build/graphql/generated/gist.d.ts.map +1 -0
  93. package/build/graphql/generated/gist.js +27 -0
  94. package/build/graphql/generated/repo.d.ts +67 -0
  95. package/build/graphql/generated/repo.d.ts.map +1 -0
  96. package/build/graphql/generated/repo.js +30 -0
  97. package/build/graphql/generated/stats.d.ts +150 -0
  98. package/build/graphql/generated/stats.d.ts.map +1 -0
  99. package/build/graphql/generated/stats.js +95 -0
  100. package/build/graphql/generated/top-languages.d.ts +54 -0
  101. package/build/graphql/generated/top-languages.d.ts.map +1 -0
  102. package/build/graphql/generated/top-languages.js +27 -0
  103. package/build/graphql/graphqlDocument.d.ts +22 -0
  104. package/build/graphql/graphqlDocument.d.ts.map +1 -0
  105. package/build/graphql/graphqlDocument.js +8 -0
  106. package/build/graphql/reposContributedToDocument.d.ts +27 -0
  107. package/build/graphql/reposContributedToDocument.d.ts.map +1 -0
  108. package/build/graphql/reposContributedToDocument.js +59 -0
  109. package/build/index.d.ts +3 -1
  110. package/build/index.d.ts.map +1 -1
  111. package/build/index.js +2 -1
  112. package/build/themes/index.d.ts +32 -0
  113. package/build/themes/index.d.ts.map +1 -1
  114. package/build/themes/index.js +28 -0
  115. package/build/translations.d.ts +17 -0
  116. package/build/translations.d.ts.map +1 -1
  117. package/build/translations.js +17 -0
  118. package/package.json +13 -7
  119. package/src/_github-username-regex.d.ts +4 -0
  120. package/src/api/gist.js +10 -46
  121. package/src/api/index.js +30 -51
  122. package/src/api/pin.js +10 -46
  123. package/src/api/top-langs.js +13 -60
  124. package/src/api/wakatime.js +10 -46
  125. package/src/cards/common-options.ts +13 -0
  126. package/src/cards/{gist.js → gist.ts} +37 -37
  127. package/src/cards/{repo.js → repo.ts} +86 -68
  128. package/src/cards/{stats.js → stats.ts} +168 -219
  129. package/src/cards/{top-languages.js → top-languages.ts} +309 -265
  130. package/src/cards/{wakatime.js → wakatime.ts} +136 -130
  131. package/src/common/Card.ts +94 -49
  132. package/src/common/I18n.ts +6 -2
  133. package/src/common/color.ts +163 -14
  134. package/src/common/config.ts +22 -29
  135. package/src/common/constants.ts +3 -1
  136. package/src/common/date.ts +45 -0
  137. package/src/common/http.ts +43 -3
  138. package/src/common/icons.ts +1 -0
  139. package/src/common/languageColors.json +1 -0
  140. package/src/common/languageColors.ts +19 -0
  141. package/src/common/ops.ts +8 -18
  142. package/src/common/render.ts +114 -14
  143. package/src/common/retryer.ts +6 -5
  144. package/src/fetchers/gist.ts +15 -72
  145. package/src/fetchers/repo.ts +114 -0
  146. package/src/fetchers/stats.ts +743 -0
  147. package/src/fetchers/top-languages.ts +135 -0
  148. package/src/fetchers/types.ts +28 -20
  149. package/src/fetchers/{wakatime.js → wakatime.ts} +19 -5
  150. package/src/graphql/contributionsDocument.ts +44 -0
  151. package/src/graphql/generated/common.ts +26 -0
  152. package/src/graphql/generated/gist.ts +59 -0
  153. package/src/graphql/generated/repo.ts +90 -0
  154. package/src/graphql/generated/stats.ts +194 -0
  155. package/src/graphql/generated/top-languages.ts +75 -0
  156. package/src/graphql/graphqlDocument.ts +25 -0
  157. package/src/graphql/queries/gist.graphql +25 -0
  158. package/src/graphql/queries/repo.graphql +28 -0
  159. package/src/graphql/queries/stats.graphql +120 -0
  160. package/src/graphql/queries/top-languages.graphql +34 -0
  161. package/src/graphql/reposContributedToDocument.ts +86 -0
  162. package/src/index.ts +3 -1
  163. package/src/themes/index.ts +33 -0
  164. package/src/translations.ts +17 -0
  165. package/src/cards/types.d.ts +0 -78
  166. package/src/fetchers/repo.js +0 -161
  167. package/src/fetchers/stats.js +0 -473
  168. package/src/fetchers/top-languages.js +0 -175
  169. package/src/themes/README.md +0 -229
@@ -1,6 +1,38 @@
1
1
  import axios from "axios";
2
2
  import type { AxiosRequestConfig, AxiosResponse } from "axios";
3
3
 
4
+ import type { GraphQLDocument } from "../graphql/graphqlDocument.js";
5
+
6
+ const GITHUB_GRAPHQL_API = "https://api.github.com/graphql";
7
+
8
+ /** Response of a GraphQL call: the envelope the GitHub API wraps results in. */
9
+ type GraphQLResponse<TResult> = AxiosResponse<{
10
+ data: TResult;
11
+ errors?: Array<{ type?: string; message?: string }>;
12
+ }>;
13
+
14
+ /**
15
+ * @param document Generated query document.
16
+ * @param scheme `Authorization` scheme for the token.
17
+ * @returns A fetcher `retryer` can drive.
18
+ */
19
+ const createGraphQLFetcher = <TResult, TVariables>(
20
+ document: GraphQLDocument<TResult, TVariables>,
21
+ scheme: "bearer" | "token",
22
+ ) => {
23
+ return (
24
+ variables: TVariables,
25
+ token: string,
26
+ ): Promise<GraphQLResponse<TResult>> => {
27
+ return axios({
28
+ url: GITHUB_GRAPHQL_API,
29
+ method: "post",
30
+ headers: { Authorization: `${scheme} ${token}` },
31
+ data: { query: document.text, variables },
32
+ });
33
+ };
34
+ };
35
+
4
36
  /** Body of a GraphQL request sent to the GitHub API. */
5
37
  interface GraphQLRequest {
6
38
  /** The GraphQL query. */
@@ -10,22 +42,30 @@ interface GraphQLRequest {
10
42
  }
11
43
 
12
44
  /**
13
- * Send GraphQL request to GitHub API.
45
+ * Send an untyped GraphQL request to the GitHub API.
14
46
  *
15
47
  * @param data Request data.
16
48
  * @param headers Request headers.
17
49
  * @returns Request response.
50
+ *
51
+ * @description Superseded by {@link createGraphQLFetcher}.
52
+ * Still used by the backend status endpoints for their own `rateLimit` query,
53
+ * and part of this package's published API.
54
+ *
55
+ * @todo consider dropping this once those endpoints post through axios directly,
56
+ * which they already depend on.
18
57
  */
19
58
  const request = (
20
59
  data: GraphQLRequest,
21
60
  headers: NonNullable<AxiosRequestConfig["headers"]>,
22
61
  ): Promise<AxiosResponse> => {
23
62
  return axios({
24
- url: "https://api.github.com/graphql",
63
+ url: GITHUB_GRAPHQL_API,
25
64
  method: "post",
26
65
  headers,
27
66
  data,
28
67
  });
29
68
  };
30
69
 
31
- export { request };
70
+ export { createGraphQLFetcher, request };
71
+ export type { GraphQLResponse };
@@ -38,6 +38,7 @@ const icons = {
38
38
  discussions_started: `<path fill-rule="evenodd" d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z" />`,
39
39
  discussions_answered: `<path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />`,
40
40
  comments: `<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />`,
41
+ contributions: `<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7.25-3.25v2.5h2.5a.75.75 0 0 1 0 1.5h-2.5v2.5a.75.75 0 0 1-1.5 0v-2.5h-2.5a.75.75 0 0 1 0-1.5h2.5v-2.5a.75.75 0 0 1 1.5 0Z"/>`,
41
42
  gist: `<path fill-rule="evenodd" d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z" />`,
42
43
  };
43
44
 
@@ -212,6 +212,7 @@
212
212
  "Glimmer JS": "#F5835F",
213
213
  "Glimmer TS": "#3178c6",
214
214
  "Glyph": "#c1ac7f",
215
+ "Gno": "#226c57",
215
216
  "Gnuplot": "#f0a9f0",
216
217
  "Go": "#00ADD8",
217
218
  "Go Checksums": "#00ADD8",
@@ -0,0 +1,19 @@
1
+ import languageColorsJson from "./languageColors.json" with { type: "json" };
2
+
3
+ /** Shown for languages missing from the generated table, and for a null API color. */
4
+ const DEFAULT_LANG_COLOR = "#858585";
5
+
6
+ // The generated JSON types has literal keys; index it as a plain lookup table.
7
+ const languageColors: Record<string, string> = languageColorsJson;
8
+
9
+ /**
10
+ * Resolves a language's brand color from the generated table.
11
+ *
12
+ * @param name Language name as GitHub spells it.
13
+ * @returns The language's hex color, or the default gray when it is unknown.
14
+ */
15
+ const getLanguageColor = (name: string): string => {
16
+ return languageColors[name] ?? DEFAULT_LANG_COLOR;
17
+ };
18
+
19
+ export { DEFAULT_LANG_COLOR, getLanguageColor };
package/src/common/ops.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import toEmoji from "emoji-name-map";
2
2
 
3
+ import type { RepositoryAffiliation } from "../graphql/generated/common.js";
4
+
3
5
  import { OWNER_AFFILIATIONS } from "./constants.js";
4
6
  import { CustomError } from "./error.js";
5
7
 
@@ -97,19 +99,8 @@ const parseEmojis = (str: string): string => {
97
99
  });
98
100
  };
99
101
 
100
- /**
101
- * Get diff in minutes between two dates.
102
- *
103
- * @param d1 First date.
104
- * @param d2 Second date.
105
- * @returns Number of minutes between the two dates.
106
- */
107
- const dateDiff = (d1: Date, d2: Date): number => {
108
- const date1 = new Date(d1);
109
- const date2 = new Date(d2);
110
- const diff = date1.getTime() - date2.getTime();
111
- return Math.round(diff / (1000 * 60));
112
- };
102
+ const isOwnerAffiliation = (value: string): value is RepositoryAffiliation =>
103
+ OWNER_AFFILIATIONS.some((affiliation) => affiliation === value);
113
104
 
114
105
  /**
115
106
  * Parse owner affiliations.
@@ -119,7 +110,9 @@ const dateDiff = (d1: Date, d2: Date): number => {
119
110
  *
120
111
  * @throws {CustomError} If affiliations contains invalid values.
121
112
  */
122
- const parseOwnerAffiliations = (affiliations: Array<string>): Array<string> => {
113
+ const parseOwnerAffiliations = (
114
+ affiliations: Array<string>,
115
+ ): Array<RepositoryAffiliation> => {
123
116
  // Set default value for ownerAffiliations.
124
117
  // NOTE: Done here since parseArray() will always return an empty array even nothing
125
118
  //was specified.
@@ -129,9 +122,7 @@ const parseOwnerAffiliations = (affiliations: Array<string>): Array<string> => {
129
122
  : ["OWNER"];
130
123
 
131
124
  // Check if ownerAffiliations contains valid values.
132
- if (
133
- normalized.some((affiliation) => !OWNER_AFFILIATIONS.includes(affiliation))
134
- ) {
125
+ if (!normalized.every(isOwnerAffiliation)) {
135
126
  throw new CustomError(
136
127
  "Invalid query parameter",
137
128
  CustomError.INVALID_AFFILIATION,
@@ -162,7 +153,6 @@ export {
162
153
  lowercaseTrim,
163
154
  chunkArray,
164
155
  parseEmojis,
165
- dateDiff,
166
156
  parseOwnerAffiliations,
167
157
  buildSearchFilter,
168
158
  };
@@ -1,5 +1,6 @@
1
1
  import { getCardColors, isPrefixedHexColor } from "./color.js";
2
2
  import { SECONDARY_ERROR_MESSAGES, TRY_AGAIN_LATER } from "./error.js";
3
+ import { kFormatter } from "./fmt.js";
3
4
  import { encodeHTML } from "./html.js";
4
5
  import { clampValue } from "./ops.js";
5
6
 
@@ -71,9 +72,9 @@ const createLanguageNode = (langName: string, langColor: string): string => {
71
72
  * @param params.x X-axis position.
72
73
  * @param params.y Y-axis position.
73
74
  * @param params.width Width of progress bar.
74
- * @param params.color Progress color.
75
+ * @param params.color Optional foreground color as an inline fill fallback.
76
+ * When omitted, the color must be set via a `.lang-progress` CSS rule.
75
77
  * @param params.progress Progress value.
76
- * @param params.progressBarBackgroundColor Progress bar bg color.
77
78
  * @param params.delay Delay before animation starts.
78
79
  * @returns Progress node.
79
80
  */
@@ -83,25 +84,18 @@ const createProgressNode = ({
83
84
  width,
84
85
  color,
85
86
  progress,
86
- progressBarBackgroundColor,
87
87
  delay,
88
88
  }: {
89
89
  x: number;
90
90
  y: number;
91
91
  width: number;
92
- color: string;
92
+ color?: string;
93
93
  progress: number;
94
- progressBarBackgroundColor: string;
95
94
  delay: number;
96
95
  }): string => {
97
- if (!isPrefixedHexColor(color)) {
96
+ if (color !== undefined && !isPrefixedHexColor(color)) {
98
97
  throw new Error(`Invalid progress color: "${color}"`);
99
98
  }
100
- if (!isPrefixedHexColor(progressBarBackgroundColor)) {
101
- throw new Error(
102
- `Invalid progress bar background color: "${progressBarBackgroundColor}"`,
103
- );
104
- }
105
99
  if (!Number.isFinite(width)) {
106
100
  throw new Error(`Invalid width: "${width}"`);
107
101
  }
@@ -119,11 +113,11 @@ const createProgressNode = ({
119
113
 
120
114
  return `
121
115
  <svg width="${width}" x="${x}" y="${y}">
122
- <rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="${width}" height="8" fill="${progressBarBackgroundColor}"></rect>
116
+ <rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="${width}" height="8"></rect>
123
117
  <svg data-testid="lang-progress" width="${progressPercentage}%">
124
118
  <rect
125
119
  height="8"
126
- fill="${color}"
120
+ ${color === undefined ? "" : `fill="${color}"`}
127
121
  rx="5" ry="5" x="0" y="0"
128
122
  class="lang-progress"
129
123
  style="animation-delay: ${delay}ms;"
@@ -265,6 +259,112 @@ const iconWithLabel = (
265
259
  return flexLayout({ items: [iconSvg, text], gap: 20 }).join("");
266
260
  };
267
261
 
262
+ /**
263
+ * Create a labelled stat text item
264
+ * (a label and its value, optionally with an icon and link).
265
+ * Shared by the stats and repo cards.
266
+ *
267
+ * The caller must ensure that the passed `icon` and `link` are properly sanitized!
268
+ *
269
+ * @param params Object that contains the createTextNode parameters.
270
+ * @param params.icon The sanitized icon to display.
271
+ * @param params.label The label to display.
272
+ * @param params.value The value to display.
273
+ * @param params.id The id of the stat.
274
+ * @param params.unitSymbol The unit symbol of the stat.
275
+ * @param params.index The index of the stat.
276
+ * @param params.showIcons Whether to show icons.
277
+ * @param params.shiftValuePos Number of pixels the value has to be shifted to the right.
278
+ * @param params.bold Whether to bold the label.
279
+ * @param params.numberFormat The format of numbers on card.
280
+ * @param params.numberPrecision The precision of numbers on card.
281
+ * @param params.link Sanitized url to link to.
282
+ * @param params.labelXOffset horizontal offset for label.
283
+ * @returns The stats card text item SVG object.
284
+ */
285
+ const createTextNode = ({
286
+ icon,
287
+ label,
288
+ value,
289
+ id,
290
+ unitSymbol,
291
+ index,
292
+ showIcons,
293
+ shiftValuePos,
294
+ bold,
295
+ numberFormat,
296
+ numberPrecision,
297
+ link,
298
+ labelXOffset = 25,
299
+ }: {
300
+ icon: string;
301
+ label: string;
302
+ value: number | string;
303
+ id: string;
304
+ // `| undefined`: card callers forward possibly-undefined per-stat fields
305
+ unitSymbol?: string | undefined;
306
+ index: number;
307
+ showIcons: boolean;
308
+ shiftValuePos: number;
309
+ bold: boolean;
310
+ numberFormat: string;
311
+ numberPrecision?: number | undefined;
312
+ link?: string | undefined;
313
+ labelXOffset?: number;
314
+ }): string => {
315
+ if (!Number.isFinite(labelXOffset)) {
316
+ throw new Error(`Invalid labelXOffset: "${labelXOffset}"`);
317
+ }
318
+ if (!Number.isFinite(shiftValuePos)) {
319
+ throw new Error(`Invalid shiftValuePos: "${shiftValuePos}"`);
320
+ }
321
+ if (!Number.isFinite(index)) {
322
+ throw new Error(`Invalid index: "${index}"`);
323
+ }
324
+
325
+ const precision =
326
+ numberPrecision !== undefined && Number.isFinite(numberPrecision)
327
+ ? clampValue(numberPrecision, 0, 2)
328
+ : undefined;
329
+ // `long` format and the percentage stat keep the raw value; everything else
330
+ // (always a number here) is abbreviated via kFormatter.
331
+ const rawValue =
332
+ numberFormat.toLowerCase() === "long" || id === "prs_merged_percentage";
333
+ const kValue =
334
+ rawValue || typeof value !== "number"
335
+ ? value
336
+ : kFormatter(value, precision);
337
+
338
+ const staggerDelay = (index + 3) * 150;
339
+ const boldClass = bold ? " bold" : "not_bold";
340
+ const valueX = (showIcons ? 140 : 120) + (bold ? 5 : 0) + shiftValuePos;
341
+ const unit = unitSymbol ? ` ${unitSymbol}` : "";
342
+ const labelOffset = showIcons ? `x="${labelXOffset}"` : "";
343
+ const iconSvg = showIcons
344
+ ? `
345
+ <svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
346
+ ${icon}
347
+ </svg>
348
+ `
349
+ : "";
350
+
351
+ const content = `
352
+ ${iconSvg}
353
+ <text class="stat ${boldClass}" ${labelOffset} y="12.5">${encodeHTML(label)}:</text>
354
+ <text
355
+ class="stat ${boldClass}"
356
+ x="${valueX}"
357
+ y="12.5"
358
+ data-testid="${id}"
359
+ >${kValue}${unit}</text>`;
360
+ const inner = link ? `<a href="${link}">${content}</a>` : content;
361
+
362
+ return `
363
+ <g class="stagger" style="animation-delay: ${staggerDelay}ms" transform="translate(25, 0)">${inner}
364
+ </g>
365
+ `;
366
+ };
367
+
268
368
  // Script parameters.
269
369
  const ERROR_CARD_LENGTH = 576.5;
270
370
 
@@ -313,7 +413,6 @@ const renderError = ({
313
413
  show_repo_link = true,
314
414
  } = renderOptions;
315
415
 
316
- // returns theme based colors with proper overrides and defaults
317
416
  const { titleColor, textColor, bgColor, borderColor } = getCardColors({
318
417
  title_color,
319
418
  text_color,
@@ -542,6 +641,7 @@ export {
542
641
  renderError,
543
642
  createLanguageNode,
544
643
  createProgressNode,
644
+ createTextNode,
545
645
  iconWithLabel,
546
646
  flexLayout,
547
647
  measureText,
@@ -36,8 +36,8 @@ function getRandomInt(max: number): number {
36
36
  */
37
37
  type FetcherResponse<TData = unknown> = AxiosResponse<TData & ResponseErrors>;
38
38
 
39
- type FetcherFunction<TData = unknown> = (
40
- variables: Record<string, unknown>,
39
+ type FetcherFunction<TData = unknown, TVariables = Record<string, unknown>> = (
40
+ variables: TVariables,
41
41
  token: string,
42
42
  retriesForTests?: number,
43
43
  ) => Promise<FetcherResponse<TData>>;
@@ -46,14 +46,15 @@ type FetcherFunction<TData = unknown> = (
46
46
  * Try to execute the fetcher function until it succeeds or the max number of retries is reached.
47
47
  *
48
48
  * @template TData Shape of `response.data` returned by the fetcher.
49
+ * @template TVariables Variables the fetcher accepts.
49
50
  * @param fetcher The fetcher function.
50
51
  * @param variables Object with arguments to pass to the fetcher function.
51
52
  * @param pat Optional PAT override.
52
53
  * @returns The response from the fetcher function.
53
54
  */
54
- const retryer = async <TData = unknown>(
55
- fetcher: FetcherFunction<TData>,
56
- variables: Record<string, unknown>,
55
+ const retryer = async <TData = unknown, TVariables = Record<string, unknown>>(
56
+ fetcher: FetcherFunction<TData, TVariables>,
57
+ variables: TVariables,
57
58
  pat: string | null = null,
58
59
  ): Promise<FetcherResponse<TData>> => {
59
60
  const PATs = pat
@@ -1,73 +1,12 @@
1
- import type { AxiosResponse } from "axios";
2
-
3
1
  import { MissingParamError } from "../common/error.js";
4
- import { request } from "../common/http.js";
2
+ import { createGraphQLFetcher } from "../common/http.js";
5
3
  import { retryer } from "../common/retryer.js";
4
+ import { GistInfoDocument } from "../graphql/generated/gist.js";
5
+ import type { GistFileInfoFragment } from "../graphql/generated/gist.js";
6
6
 
7
7
  import type { GistData } from "./types.js";
8
8
 
9
- const QUERY = `
10
- query gistInfo($gistName: String!) {
11
- viewer {
12
- gist(name: $gistName) {
13
- description
14
- owner {
15
- login
16
- }
17
- stargazerCount
18
- forks {
19
- totalCount
20
- }
21
- files {
22
- name
23
- language {
24
- name
25
- }
26
- size
27
- }
28
- }
29
- }
30
- }
31
- `;
32
-
33
- /**
34
- * Gist data fetcher.
35
- *
36
- * @param variables Fetcher variables.
37
- * @param token GitHub token.
38
- * @returns The response.
39
- */
40
- const fetcher = (
41
- variables: Record<string, unknown>,
42
- token: string,
43
- ): Promise<AxiosResponse> => {
44
- return request(
45
- { query: QUERY, variables },
46
- { Authorization: `token ${token}` },
47
- );
48
- };
49
-
50
- /** A single file within a gist. */
51
- interface GistFile {
52
- name: string;
53
- language: { name: string } | null;
54
- size: number;
55
- }
56
-
57
- /** Shape of `response.data` returned by the gist GraphQL query. */
58
- interface GistQueryResponse {
59
- data: {
60
- viewer: {
61
- gist: {
62
- description: string | null;
63
- owner: { login: string };
64
- stargazerCount: number;
65
- forks: { totalCount: number };
66
- files: Array<GistFile>;
67
- } | null;
68
- };
69
- };
70
- }
9
+ const fetcher = createGraphQLFetcher(GistInfoDocument, "token");
71
10
 
72
11
  /**
73
12
  * This function calculates the primary language of a gist by files size.
@@ -75,13 +14,15 @@ interface GistQueryResponse {
75
14
  * @param files Files.
76
15
  * @returns Primary language, or `null` when no file has a language.
77
16
  */
78
- const calculatePrimaryLanguage = (files: Array<GistFile>): string | null => {
17
+ const calculatePrimaryLanguage = (
18
+ files: Array<GistFileInfoFragment>,
19
+ ): string | null => {
79
20
  const languages: Record<string, number> = {};
80
21
 
81
22
  for (const file of files) {
82
23
  if (file.language) {
83
24
  languages[file.language.name] =
84
- (languages[file.language.name] ?? 0) + file.size;
25
+ (languages[file.language.name] ?? 0) + (file.size ?? 0);
85
26
  }
86
27
  }
87
28
 
@@ -111,7 +52,7 @@ const fetchGist = async (
111
52
  if (!id) {
112
53
  throw new MissingParamError(["id"], "/api/gist?id=GIST_ID");
113
54
  }
114
- const res = await retryer<GistQueryResponse>(fetcher, { gistName: id }, pat);
55
+ const res = await retryer(fetcher, { gistName: id }, pat);
115
56
  if (res.data.errors) {
116
57
  throw new Error(res.data.errors[0]?.message);
117
58
  }
@@ -119,15 +60,17 @@ const fetchGist = async (
119
60
  if (!gist) {
120
61
  throw new Error("Gist not found");
121
62
  }
122
- const firstFile = gist.files[0];
123
- if (!firstFile) {
63
+ const firstFile = gist.files?.[0];
64
+ if (!firstFile?.name) {
124
65
  throw new Error("Gist has no files");
125
66
  }
126
67
  return {
127
68
  name: firstFile.name,
128
- nameWithOwner: `${gist.owner.login}/${firstFile.name}`,
69
+ nameWithOwner: `${gist.owner?.login ?? ""}/${firstFile.name}`,
129
70
  description: gist.description,
130
- language: calculatePrimaryLanguage(gist.files),
71
+ language: calculatePrimaryLanguage(
72
+ gist.files?.filter((file) => !!file) ?? [],
73
+ ),
131
74
  starsCount: gist.stargazerCount,
132
75
  forksCount: gist.forks.totalCount,
133
76
  };
@@ -0,0 +1,114 @@
1
+ import { MissingParamError } from "../common/error.js";
2
+ import { createGraphQLFetcher } from "../common/http.js";
3
+ import { retryer } from "../common/retryer.js";
4
+ import { GetRepoDocument } from "../graphql/generated/repo.js";
5
+
6
+ import { fetchRepoUserStats } from "./stats.js";
7
+ import type { RepositoryData } from "./types.js";
8
+
9
+ const fetcher = createGraphQLFetcher(GetRepoDocument, "token");
10
+
11
+ const urlExample = "/api/pin?username=USERNAME&repo=REPO_NAME";
12
+
13
+ /**
14
+ * Fetch repository data.
15
+ *
16
+ * @param username GitHub username.
17
+ * @param reponame GitHub repository name.
18
+ * @param include_prs_authored Include count of PRs authored.
19
+ * @param include_prs_commented Include count of PRs commented.
20
+ * @param include_prs_reviewed Include count of PRs reviewed.
21
+ * @param include_issues_authored Include count of issues authored.
22
+ * @param include_issues_commented Include count of issues commented.
23
+ * @param pat Optional PAT override.
24
+ * @returns Repository data.
25
+ */
26
+ const fetchRepo = async (
27
+ username: string,
28
+ reponame: string,
29
+ include_prs_authored = false,
30
+ include_prs_commented = false,
31
+ include_prs_reviewed = false,
32
+ include_issues_authored = false,
33
+ include_issues_commented = false,
34
+ pat: string | null = null,
35
+ ): Promise<RepositoryData> => {
36
+ let owner = username;
37
+ if (reponame && reponame.includes("/")) {
38
+ const [parsedOwner, parsedRepo] = reponame.split("/");
39
+ owner = parsedOwner ?? "";
40
+ reponame = parsedRepo ?? "";
41
+ }
42
+
43
+ if (owner && !username) {
44
+ username = owner;
45
+ }
46
+ if (username && !owner) {
47
+ owner = username;
48
+ }
49
+ if (!username && !reponame) {
50
+ throw new MissingParamError(["username", "repo"], urlExample);
51
+ }
52
+ if (!username) {
53
+ throw new MissingParamError(["username"], urlExample);
54
+ }
55
+ if (!reponame) {
56
+ throw new MissingParamError(["repo"], urlExample);
57
+ }
58
+
59
+ const res = await retryer(fetcher, { login: owner, repo: reponame }, pat);
60
+
61
+ const data = res.data.data;
62
+
63
+ if (!data.user && !data.organization) {
64
+ throw new Error("Not found");
65
+ }
66
+
67
+ if (data.organization === null && data.user) {
68
+ const repository = data.user.repository;
69
+ if (!repository || repository.isPrivate) {
70
+ throw new Error("User Repository Not found");
71
+ }
72
+ const repoUserStats = await fetchRepoUserStats(
73
+ username,
74
+ [owner + "/" + reponame],
75
+ [],
76
+ include_prs_authored,
77
+ include_prs_commented,
78
+ include_prs_reviewed,
79
+ include_issues_authored,
80
+ include_issues_commented,
81
+ pat,
82
+ );
83
+ return {
84
+ ...repoUserStats,
85
+ ...repository,
86
+ };
87
+ }
88
+
89
+ if (data.user === null && data.organization) {
90
+ const repository = data.organization.repository;
91
+ if (!repository || repository.isPrivate) {
92
+ throw new Error("Organization Repository Not found");
93
+ }
94
+ const repoUserStats = await fetchRepoUserStats(
95
+ username,
96
+ [owner + "/" + reponame],
97
+ [],
98
+ include_prs_authored,
99
+ include_prs_commented,
100
+ include_prs_reviewed,
101
+ include_issues_authored,
102
+ include_issues_commented,
103
+ pat,
104
+ );
105
+ return {
106
+ ...repoUserStats,
107
+ ...repository,
108
+ };
109
+ }
110
+
111
+ throw new Error("Unexpected behavior");
112
+ };
113
+
114
+ export { fetchRepo };