@stats-forge/github-stats-forge-core 0.0.1

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 (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/build/api/api-result.d.ts +44 -0
  4. package/build/api/api-result.d.ts.map +1 -0
  5. package/build/api/api-result.js +33 -0
  6. package/build/api/gist.d.ts +29 -0
  7. package/build/api/gist.d.ts.map +1 -0
  8. package/build/api/gist.js +52 -0
  9. package/build/api/index.d.ts +13 -0
  10. package/build/api/index.d.ts.map +1 -0
  11. package/build/api/index.js +8 -0
  12. package/build/api/params.d.ts +93 -0
  13. package/build/api/params.d.ts.map +1 -0
  14. package/build/api/params.js +145 -0
  15. package/build/api/pin.d.ts +47 -0
  16. package/build/api/pin.d.ts.map +1 -0
  17. package/build/api/pin.js +87 -0
  18. package/build/api/stats.d.ts +42 -0
  19. package/build/api/stats.d.ts.map +1 -0
  20. package/build/api/stats.js +132 -0
  21. package/build/api/top-langs.d.ts +37 -0
  22. package/build/api/top-langs.d.ts.map +1 -0
  23. package/build/api/top-langs.js +101 -0
  24. package/build/api/wakatime.d.ts +34 -0
  25. package/build/api/wakatime.d.ts.map +1 -0
  26. package/build/api/wakatime.js +91 -0
  27. package/build/calculateRank.d.ts +29 -0
  28. package/build/calculateRank.d.ts.map +1 -0
  29. package/build/calculateRank.js +58 -0
  30. package/build/cards/gist.d.ts +16 -0
  31. package/build/cards/gist.d.ts.map +1 -0
  32. package/build/cards/gist.js +119 -0
  33. package/build/cards/index.d.ts +11 -0
  34. package/build/cards/index.d.ts.map +1 -0
  35. package/build/cards/index.js +7 -0
  36. package/build/cards/options.d.ts +19 -0
  37. package/build/cards/options.d.ts.map +1 -0
  38. package/build/cards/options.js +1 -0
  39. package/build/cards/repo.d.ts +25 -0
  40. package/build/cards/repo.d.ts.map +1 -0
  41. package/build/cards/repo.js +276 -0
  42. package/build/cards/stats.d.ts +37 -0
  43. package/build/cards/stats.d.ts.map +1 -0
  44. package/build/cards/stats.js +457 -0
  45. package/build/cards/top-languages.d.ts +153 -0
  46. package/build/cards/top-languages.d.ts.map +1 -0
  47. package/build/cards/top-languages.js +805 -0
  48. package/build/cards/wakatime.d.ts +31 -0
  49. package/build/cards/wakatime.d.ts.map +1 -0
  50. package/build/cards/wakatime.js +376 -0
  51. package/build/common/Card.d.ts +121 -0
  52. package/build/common/Card.d.ts.map +1 -0
  53. package/build/common/Card.js +305 -0
  54. package/build/common/I18n.d.ts +27 -0
  55. package/build/common/I18n.d.ts.map +1 -0
  56. package/build/common/I18n.js +35 -0
  57. package/build/common/color.d.ts +112 -0
  58. package/build/common/color.d.ts.map +1 -0
  59. package/build/common/color.js +188 -0
  60. package/build/common/config.d.ts +53 -0
  61. package/build/common/config.d.ts.map +1 -0
  62. package/build/common/config.js +87 -0
  63. package/build/common/constants.d.ts +7 -0
  64. package/build/common/constants.d.ts.map +1 -0
  65. package/build/common/constants.js +9 -0
  66. package/build/common/date.d.ts +29 -0
  67. package/build/common/date.d.ts.map +1 -0
  68. package/build/common/date.js +23 -0
  69. package/build/common/error.d.ts +87 -0
  70. package/build/common/error.d.ts.map +1 -0
  71. package/build/common/error.js +108 -0
  72. package/build/common/fmt.d.ts +28 -0
  73. package/build/common/fmt.d.ts.map +1 -0
  74. package/build/common/fmt.js +68 -0
  75. package/build/common/html.d.ts +8 -0
  76. package/build/common/html.d.ts.map +1 -0
  77. package/build/common/html.js +14 -0
  78. package/build/common/http.d.ts +53 -0
  79. package/build/common/http.d.ts.map +1 -0
  80. package/build/common/http.js +47 -0
  81. package/build/common/icons.d.ts +28 -0
  82. package/build/common/icons.d.ts.map +1 -0
  83. package/build/common/icons.js +77 -0
  84. package/build/common/languageColors.d.ts +11 -0
  85. package/build/common/languageColors.d.ts.map +1 -0
  86. package/build/common/languageColors.js +15 -0
  87. package/build/common/languageColors.json +678 -0
  88. package/build/common/log.d.ts +9 -0
  89. package/build/common/log.d.ts.map +1 -0
  90. package/build/common/log.js +8 -0
  91. package/build/common/ops.d.ts +59 -0
  92. package/build/common/ops.d.ts.map +1 -0
  93. package/build/common/ops.js +115 -0
  94. package/build/common/render.d.ts +196 -0
  95. package/build/common/render.d.ts.map +1 -0
  96. package/build/common/render.js +474 -0
  97. package/build/common/retryer.d.ts +36 -0
  98. package/build/common/retryer.d.ts.map +1 -0
  99. package/build/common/retryer.js +67 -0
  100. package/build/fetchers/gist.d.ts +15 -0
  101. package/build/fetchers/gist.d.ts.map +1 -0
  102. package/build/fetchers/gist.js +68 -0
  103. package/build/fetchers/index.d.ts +13 -0
  104. package/build/fetchers/index.d.ts.map +1 -0
  105. package/build/fetchers/index.js +10 -0
  106. package/build/fetchers/repo.d.ts +27 -0
  107. package/build/fetchers/repo.d.ts.map +1 -0
  108. package/build/fetchers/repo.js +100 -0
  109. package/build/fetchers/stats.d.ts +74 -0
  110. package/build/fetchers/stats.d.ts.map +1 -0
  111. package/build/fetchers/stats.js +498 -0
  112. package/build/fetchers/top-languages.d.ts +23 -0
  113. package/build/fetchers/top-languages.d.ts.map +1 -0
  114. package/build/fetchers/top-languages.js +89 -0
  115. package/build/fetchers/types.d.ts +130 -0
  116. package/build/fetchers/types.d.ts.map +1 -0
  117. package/build/fetchers/types.js +1 -0
  118. package/build/fetchers/wakatime.d.ts +19 -0
  119. package/build/fetchers/wakatime.d.ts.map +1 -0
  120. package/build/fetchers/wakatime.js +36 -0
  121. package/build/graphql/contributionsDocument.d.ts +18 -0
  122. package/build/graphql/contributionsDocument.d.ts.map +1 -0
  123. package/build/graphql/contributionsDocument.js +30 -0
  124. package/build/graphql/generated/common.d.ts +42 -0
  125. package/build/graphql/generated/common.d.ts.map +1 -0
  126. package/build/graphql/generated/common.js +2 -0
  127. package/build/graphql/generated/gist.d.ts +44 -0
  128. package/build/graphql/generated/gist.d.ts.map +1 -0
  129. package/build/graphql/generated/gist.js +27 -0
  130. package/build/graphql/generated/repo.d.ts +67 -0
  131. package/build/graphql/generated/repo.d.ts.map +1 -0
  132. package/build/graphql/generated/repo.js +30 -0
  133. package/build/graphql/generated/stats.d.ts +150 -0
  134. package/build/graphql/generated/stats.d.ts.map +1 -0
  135. package/build/graphql/generated/stats.js +95 -0
  136. package/build/graphql/generated/top-languages.d.ts +54 -0
  137. package/build/graphql/generated/top-languages.d.ts.map +1 -0
  138. package/build/graphql/generated/top-languages.js +27 -0
  139. package/build/graphql/graphqlDocument.d.ts +22 -0
  140. package/build/graphql/graphqlDocument.d.ts.map +1 -0
  141. package/build/graphql/graphqlDocument.js +8 -0
  142. package/build/graphql/reposContributedToDocument.d.ts +27 -0
  143. package/build/graphql/reposContributedToDocument.d.ts.map +1 -0
  144. package/build/graphql/reposContributedToDocument.js +59 -0
  145. package/build/index.d.ts +15 -0
  146. package/build/index.d.ts.map +1 -0
  147. package/build/index.js +10 -0
  148. package/build/themes/index.d.ts +505 -0
  149. package/build/themes/index.d.ts.map +1 -0
  150. package/build/themes/index.js +505 -0
  151. package/build/translations.d.ts +1153 -0
  152. package/build/translations.d.ts.map +1 -0
  153. package/build/translations.js +1156 -0
  154. package/package.json +114 -0
  155. package/src/_emoji-name-map.d.ts +10 -0
  156. package/src/_github-username-regex.d.ts +4 -0
  157. package/src/api/api-result.ts +69 -0
  158. package/src/api/gist.ts +66 -0
  159. package/src/api/index.ts +16 -0
  160. package/src/api/params.ts +221 -0
  161. package/src/api/pin.ts +126 -0
  162. package/src/api/stats.ts +195 -0
  163. package/src/api/top-langs.ts +150 -0
  164. package/src/api/wakatime.ts +130 -0
  165. package/src/calculateRank.ts +91 -0
  166. package/src/cards/gist.ts +169 -0
  167. package/src/cards/index.ts +21 -0
  168. package/src/cards/options.ts +18 -0
  169. package/src/cards/repo.ts +376 -0
  170. package/src/cards/stats.ts +615 -0
  171. package/src/cards/top-languages.ts +1077 -0
  172. package/src/cards/wakatime.ts +501 -0
  173. package/src/common/Card.ts +364 -0
  174. package/src/common/I18n.ts +51 -0
  175. package/src/common/color.ts +296 -0
  176. package/src/common/config.ts +118 -0
  177. package/src/common/constants.ts +12 -0
  178. package/src/common/date.ts +34 -0
  179. package/src/common/error.ts +163 -0
  180. package/src/common/fmt.ts +86 -0
  181. package/src/common/html.ts +17 -0
  182. package/src/common/http.ts +98 -0
  183. package/src/common/icons.ts +80 -0
  184. package/src/common/languageColors.json +678 -0
  185. package/src/common/languageColors.ts +19 -0
  186. package/src/common/log.ts +12 -0
  187. package/src/common/ops.ts +145 -0
  188. package/src/common/render.ts +641 -0
  189. package/src/common/retryer.ts +111 -0
  190. package/src/fetchers/gist.ts +82 -0
  191. package/src/fetchers/index.ts +25 -0
  192. package/src/fetchers/repo.ts +140 -0
  193. package/src/fetchers/stats.ts +742 -0
  194. package/src/fetchers/top-languages.ts +134 -0
  195. package/src/fetchers/types.ts +136 -0
  196. package/src/fetchers/wakatime.ts +56 -0
  197. package/src/graphql/contributionsDocument.ts +44 -0
  198. package/src/graphql/generated/common.ts +26 -0
  199. package/src/graphql/generated/gist.ts +56 -0
  200. package/src/graphql/generated/repo.ts +79 -0
  201. package/src/graphql/generated/stats.ts +180 -0
  202. package/src/graphql/generated/top-languages.ts +66 -0
  203. package/src/graphql/graphqlDocument.ts +25 -0
  204. package/src/graphql/queries/gist.graphql +25 -0
  205. package/src/graphql/queries/repo.graphql +28 -0
  206. package/src/graphql/queries/stats.graphql +113 -0
  207. package/src/graphql/queries/top-languages.graphql +27 -0
  208. package/src/graphql/reposContributedToDocument.ts +83 -0
  209. package/src/index.ts +18 -0
  210. package/src/themes/index.ts +519 -0
  211. package/src/translations.ts +1169 -0
@@ -0,0 +1,118 @@
1
+ import { defaultFetch } from './http.js';
2
+ import type { FetchLike } from './http.js';
3
+
4
+ type Env = Record<string, string | undefined>;
5
+
6
+ interface PersonalAccessToken {
7
+ /** Env variable the token came from — the retryer logs this name, never the value. */
8
+ name: string;
9
+ value: string;
10
+ }
11
+
12
+ /** Overrides for a {@link CardConfig}; the constructor defaults anything omitted. */
13
+ interface CardConfigInit {
14
+ pats?: ReadonlyArray<PersonalAccessToken>;
15
+ usernameAllowlist?: ReadonlyArray<string> | undefined;
16
+ gistAllowlist?: ReadonlyArray<string> | undefined;
17
+ excludeRepositories?: ReadonlyArray<string>;
18
+ fetchMultiPageStars?: number;
19
+ fetch?: FetchLike;
20
+ }
21
+
22
+ /**
23
+ * @param value Comma-separated string.
24
+ * @returns Parsed string values.
25
+ */
26
+ const parseCsv = (value: string | undefined): Array<string> | undefined =>
27
+ value ? value.split(',') : undefined;
28
+
29
+ /**
30
+ * @param value Raw `FETCH_MULTI_PAGE_STARS` value.
31
+ * @returns Page limit: `"true"` means every page, a positive number caps the pages, anything else means one.
32
+ */
33
+ const parseFetchMultiPageStars = (value: string | undefined): number => {
34
+ if (value === 'true') {
35
+ return Infinity;
36
+ }
37
+ const limit = Number(value);
38
+ return limit > 0 ? limit : 1;
39
+ };
40
+
41
+ /**
42
+ * @param env Environment variables to inspect.
43
+ * @returns Personal access tokens found in the environment.
44
+ */
45
+ const parsePATsFromEnv = (env: Env): Array<PersonalAccessToken> =>
46
+ Object.keys(env)
47
+ .filter((key) => /PAT_\d*$/.exec(key))
48
+ .map((name) => ({ name, value: env[name] ?? '' }));
49
+
50
+ /**
51
+ * Deployment-wide configuration for the card renderers.
52
+ *
53
+ * Immutable, and built by the host rather than read from the environment, so the
54
+ * library behaves identically under Node, in the browser and under vitest.
55
+ */
56
+ export class CardConfig {
57
+ readonly pats: ReadonlyArray<PersonalAccessToken>;
58
+ /** Allowed usernames; `undefined` means no allowlist configured. */
59
+ readonly usernameAllowlist: ReadonlyArray<string> | undefined;
60
+ /** Allowed gist ids; `undefined` means no allowlist configured. */
61
+ readonly gistAllowlist: ReadonlyArray<string> | undefined;
62
+ readonly excludeRepositories: ReadonlyArray<string>;
63
+ /** Max pages of starred repos; `Infinity` means every page, `1` only the first. */
64
+ readonly fetchMultiPageStars: number;
65
+ /** Transport every fetcher sends through; defaults to `globalThis.fetch`. */
66
+ readonly fetch: FetchLike;
67
+
68
+ constructor(init: CardConfigInit = {}) {
69
+ this.pats = init.pats ?? [];
70
+ this.usernameAllowlist = init.usernameAllowlist;
71
+ this.gistAllowlist = init.gistAllowlist;
72
+ this.excludeRepositories = init.excludeRepositories ?? [];
73
+ this.fetchMultiPageStars = init.fetchMultiPageStars ?? 1;
74
+ this.fetch = init.fetch ?? defaultFetch;
75
+ }
76
+
77
+ /**
78
+ * @param env Environment variables to read — the host passes `process.env` or an equivalent.
79
+ * @returns Config for this deployment.
80
+ */
81
+ static fromEnv(env: Env): CardConfig {
82
+ return new CardConfig({
83
+ pats: parsePATsFromEnv(env),
84
+ usernameAllowlist: parseCsv(env['WHITELIST']),
85
+ gistAllowlist: parseCsv(env['GIST_WHITELIST']),
86
+ excludeRepositories: parseCsv(env['EXCLUDE_REPO']) ?? [],
87
+ fetchMultiPageStars: parseFetchMultiPageStars(env['FETCH_MULTI_PAGE_STARS']),
88
+ });
89
+ }
90
+
91
+ /**
92
+ * @param id Username or gist id being requested.
93
+ * @param kind Which allowlist applies.
94
+ * @returns Whether this deployment serves the id.
95
+ */
96
+ isAllowed(id: string, kind: 'username' | 'gist'): boolean {
97
+ const list = kind === 'gist' ? this.gistAllowlist : this.usernameAllowlist;
98
+ return list === undefined || list.includes(id);
99
+ }
100
+
101
+ /**
102
+ * @param overrides Fields to replace.
103
+ * @returns A copy with `overrides` applied — how a host swaps in a user's PAT per request.
104
+ */
105
+ with(overrides: CardConfigInit): CardConfig {
106
+ return new CardConfig({
107
+ pats: this.pats,
108
+ usernameAllowlist: this.usernameAllowlist,
109
+ gistAllowlist: this.gistAllowlist,
110
+ excludeRepositories: this.excludeRepositories,
111
+ fetchMultiPageStars: this.fetchMultiPageStars,
112
+ fetch: this.fetch,
113
+ ...overrides,
114
+ });
115
+ }
116
+ }
117
+
118
+ export type { PersonalAccessToken, CardConfigInit };
@@ -0,0 +1,12 @@
1
+ import type { RepositoryAffiliation } from '../graphql/generated/common.js';
2
+
3
+ /**
4
+ * Valid owner affiliations for GitHub API queries.
5
+ */
6
+ const OWNER_AFFILIATIONS: Array<RepositoryAffiliation> = [
7
+ 'OWNER',
8
+ 'COLLABORATOR',
9
+ 'ORGANIZATION_MEMBER',
10
+ ];
11
+
12
+ export { OWNER_AFFILIATIONS };
@@ -0,0 +1,34 @@
1
+ /** A span between two dates, as GitHub's range arguments take it. */
2
+ interface GitHubDateRange {
3
+ /** Start of the range, inclusive. */
4
+ from: Date;
5
+ /** End of the range, inclusive. */
6
+ to: Date;
7
+ }
8
+
9
+ /**
10
+ * Format a date as a GitHub `DateTime` scalar.
11
+ * Seconds precision, no milliseconds.
12
+ *
13
+ * @param date Date to format.
14
+ * @returns e.g. `2024-01-01T00:00:00Z`.
15
+ */
16
+ const toGitHubDateTime = (date: Date): string => `${date.toISOString().slice(0, 19)}Z`;
17
+
18
+ /**
19
+ * The full UTC span of a calendar year, both ends inclusive.
20
+ *
21
+ * The end matters to callers that would otherwise leave a range open:
22
+ * GitHub's `contributionsCollection` defaults `to` to one year after `from`,
23
+ * which pulls the next 1st of January into the year.
24
+ *
25
+ * @param year Four-digit year.
26
+ * @returns The range covering it.
27
+ */
28
+ const getGitHubYearRange = (year: number): GitHubDateRange => ({
29
+ from: new Date(Date.UTC(year, 0, 1)),
30
+ to: new Date(Date.UTC(year, 11, 31, 23, 59, 59)),
31
+ });
32
+
33
+ export { getGitHubYearRange, toGitHubDateTime };
34
+ export type { GitHubDateRange };
@@ -0,0 +1,163 @@
1
+ import { OWNER_AFFILIATIONS } from './constants.js';
2
+
3
+ /**
4
+ * @file One error type for everything a card render can fail on.
5
+ *
6
+ * A failure carries a `code` a host can branch on, the two lines the error card
7
+ * draws, and — through the code — whether repeating the request could help.
8
+ * The fetchers throw this rather than a bare `Error`, so a host is told what failed.
9
+ * Anything else that escapes is wrapped as `upstream` by `from`, which is retryable —
10
+ * so a permanent failure has to throw a `CardError` to be reported as one.
11
+ */
12
+
13
+ /** A general message to ask user to try again later. */
14
+ const TRY_AGAIN_LATER = 'Please try again later';
15
+
16
+ /** Why a request failed. */
17
+ type ErrorCode =
18
+ /** The query is wrong: a param is malformed, unsafe, or not renderable. */
19
+ | 'invalid_param'
20
+ /** The query is missing a param the endpoint cannot render without. */
21
+ | 'missing_param'
22
+ /** The user, repository or gist does not exist. */
23
+ | 'not_found'
24
+ /** The deployment has no usable GitHub token. */
25
+ | 'no_tokens'
26
+ /** Every token is rate limited. */
27
+ | 'rate_limited'
28
+ /** GitHub or WakaTime answered with something unusable. */
29
+ | 'upstream';
30
+
31
+ /**
32
+ * Whether repeating the request could produce a different answer.
33
+ * A host reads this to decide between caching the failure and retrying it.
34
+ */
35
+ const RETRYABLE: Record<ErrorCode, boolean> = {
36
+ invalid_param: false,
37
+ missing_param: false,
38
+ not_found: false,
39
+ no_tokens: false,
40
+ rate_limited: true,
41
+ upstream: true,
42
+ };
43
+
44
+ /** The second line the error card draws, per code. */
45
+ const SECONDARY_ERROR_MESSAGES: Partial<Record<ErrorCode, string>> = {
46
+ rate_limited: 'You can deploy own instance or wait until public will be no longer limited',
47
+ no_tokens:
48
+ 'Please add an env variable called PAT_1 with your GitHub API token in your deployment environment',
49
+ upstream: TRY_AGAIN_LATER,
50
+ };
51
+
52
+ /** What a failure is, once it reaches the api layer. */
53
+ interface CardErrorInit {
54
+ /** Why it failed. */
55
+ code: ErrorCode;
56
+ /** Second line of the error card; the code's own message when omitted. */
57
+ secondaryMessage?: string | undefined;
58
+ /** The first param at fault, when the failure names one. */
59
+ param?: string | undefined;
60
+ }
61
+
62
+ /** Everything this package throws. */
63
+ class CardError extends Error {
64
+ readonly code: ErrorCode;
65
+ readonly secondaryMessage: string | undefined;
66
+ readonly param: string | undefined;
67
+
68
+ /**
69
+ * @param message First line of the error card.
70
+ * @param init Why it failed, and what to draw under the message.
71
+ */
72
+ constructor(message: string, init: CardErrorInit) {
73
+ super(message);
74
+ this.name = 'CardError';
75
+ this.code = init.code;
76
+ this.param = init.param;
77
+ this.secondaryMessage = init.secondaryMessage ?? SECONDARY_ERROR_MESSAGES[init.code];
78
+ }
79
+
80
+ /** Whether repeating the request could produce a different answer. */
81
+ get retryable(): boolean {
82
+ return RETRYABLE[this.code];
83
+ }
84
+
85
+ /**
86
+ * A param the endpoint cannot render with.
87
+ *
88
+ * @param param Name of the param.
89
+ * @param secondaryMessage What is wrong with it.
90
+ * @returns The error.
91
+ */
92
+ static invalidParam(param: string, secondaryMessage: string): CardError {
93
+ return new CardError('Something went wrong', {
94
+ code: 'invalid_param',
95
+ secondaryMessage,
96
+ param,
97
+ });
98
+ }
99
+
100
+ /**
101
+ * A param the endpoint cannot render without.
102
+ *
103
+ * @param params Names of the missing params.
104
+ * `param` carries the first; the message names them all.
105
+ * @param secondaryMessage Where to pass them, when the endpoint can say.
106
+ * @returns The error.
107
+ */
108
+ static missingParam(params: Array<string>, secondaryMessage?: string): CardError {
109
+ const named = params.map((param) => `"${param}"`).join(', ');
110
+ return new CardError(`Missing params ${named} make sure you pass the parameters in URL`, {
111
+ code: 'missing_param',
112
+ secondaryMessage,
113
+ param: params[0],
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Anything thrown that is not already a `CardError`:
119
+ * an upstream failure, since the query itself got this far.
120
+ *
121
+ * @param err Whatever was thrown.
122
+ * @returns The error, unchanged when it already was one.
123
+ */
124
+ static from(err: unknown): CardError {
125
+ if (err instanceof CardError) {
126
+ return err;
127
+ }
128
+ if (err instanceof Error) {
129
+ return new CardError(err.message, { code: 'upstream' });
130
+ }
131
+ return new CardError('An unknown error occurred', { code: 'upstream' });
132
+ }
133
+ }
134
+
135
+ /** The affiliation values `role` accepts, named in the rejection. */
136
+ const INVALID_AFFILIATION = `Invalid owner affiliations. Valid values are: ${OWNER_AFFILIATIONS.join(
137
+ ', ',
138
+ )}`;
139
+
140
+ /** The user exists but has no public WakaTime profile. */
141
+ const WAKATIME_USER_NOT_FOUND = 'Make sure you have a public WakaTime profile';
142
+
143
+ /** A GitHub username that resolves to nothing, or to an organization. */
144
+ const USER_NOT_FOUND = 'Make sure the provided username is not an organization';
145
+
146
+ /** A repository the token cannot see, because it is missing or private. */
147
+ const REPO_NOT_FOUND = 'Make sure the provided username and repository are correct';
148
+
149
+ /** A gist id that resolves to nothing. */
150
+ const GIST_NOT_FOUND = 'Make sure the provided gist ID is correct';
151
+
152
+ export type { ErrorCode };
153
+
154
+ export {
155
+ CardError,
156
+ GIST_NOT_FOUND,
157
+ INVALID_AFFILIATION,
158
+ REPO_NOT_FOUND,
159
+ SECONDARY_ERROR_MESSAGES,
160
+ TRY_AGAIN_LATER,
161
+ USER_NOT_FOUND,
162
+ WAKATIME_USER_NOT_FOUND,
163
+ };
@@ -0,0 +1,86 @@
1
+ import { encodeHTML } from './html.js';
2
+ import { splitWrappedText } from './render.js';
3
+
4
+ /**
5
+ * Retrieves num with suffix k(thousands) precise to given decimal places.
6
+ *
7
+ * @param num The number to format.
8
+ * @param precision The number of decimal places to include.
9
+ * @returns The formatted number.
10
+ */
11
+ const kFormatter = (num: number, precision?: number): string | number => {
12
+ const abs = Math.abs(num);
13
+ const sign = Math.sign(num);
14
+
15
+ if (typeof precision === 'number' && !isNaN(precision)) {
16
+ return (sign * (abs / 1000)).toFixed(precision) + 'k';
17
+ }
18
+
19
+ if (abs < 1000) {
20
+ return sign * abs;
21
+ }
22
+
23
+ return `${sign * parseFloat((abs / 1000).toFixed(1))}k`;
24
+ };
25
+
26
+ /**
27
+ * Convert bytes to a human-readable string representation.
28
+ *
29
+ * @param bytes The number of bytes to convert.
30
+ * @returns The human-readable representation of bytes.
31
+ * @throws {Error} If bytes is negative or too large.
32
+ */
33
+ const formatBytes = (bytes: number): string => {
34
+ if (bytes < 0) {
35
+ throw new Error('Bytes must be a non-negative number');
36
+ }
37
+
38
+ if (bytes === 0) {
39
+ return '0 B';
40
+ }
41
+
42
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
43
+ const base = 1024;
44
+ const i = Math.floor(Math.log(bytes) / Math.log(base));
45
+
46
+ const unit = sizes[i];
47
+ if (unit === undefined) {
48
+ throw new Error('Bytes is too large to convert to a human-readable string');
49
+ }
50
+
51
+ return `${(bytes / Math.pow(base, i)).toFixed(1)} ${unit}`;
52
+ };
53
+
54
+ /**
55
+ * Split text over multiple lines based on the card width.
56
+ *
57
+ * @param text Text to split.
58
+ * @param width Available wrap width in px.
59
+ * @param fontSize Font size in px.
60
+ * @param maxLines Maximum number of lines.
61
+ * @returns Array of lines.
62
+ */
63
+ const wrapTextMultiline = (
64
+ text: string,
65
+ width: number,
66
+ fontSize: number,
67
+ maxLines = 3,
68
+ ): Array<string> => {
69
+ const wrapped = splitWrappedText(text, fontSize, width);
70
+ const lines = wrapped.map((line) => encodeHTML(line.trim())).slice(0, maxLines); // Only consider maxLines lines
71
+
72
+ // Add "..." to the last line if the text exceeds maxLines
73
+ if (wrapped.length > maxLines) {
74
+ const lastIndex = maxLines - 1;
75
+ const lastLine = lines[lastIndex];
76
+ if (lastLine !== undefined) {
77
+ lines[lastIndex] = `${lastLine}...`;
78
+ }
79
+ }
80
+
81
+ // Remove empty lines if text fits in less than maxLines lines
82
+ const multiLineText = lines.filter(Boolean);
83
+ return multiLineText;
84
+ };
85
+
86
+ export { kFormatter, formatBytes, wrapTextMultiline };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Encode string as HTML.
3
+ *
4
+ * @see https://stackoverflow.com/a/48073476/10629172
5
+ */
6
+ const encodeHTML = (str: string): string => {
7
+ return (
8
+ str
9
+ .replace(/[\u00A0-\u9999<>&"'](?!#)/gim, (i: string) => {
10
+ return `&#${i.charCodeAt(0)};`;
11
+ })
12
+ // eslint-disable-next-line no-control-regex
13
+ .replace(/\u0008/gim, '')
14
+ );
15
+ };
16
+
17
+ export { encodeHTML };
@@ -0,0 +1,98 @@
1
+ import type { GraphQLDocument } from '../graphql/graphqlDocument.js';
2
+
3
+ const GITHUB_GRAPHQL_API = 'https://api.github.com/graphql';
4
+
5
+ /**
6
+ * The slice of `fetch` this library calls.
7
+ * A host substitutes it through `CardConfig` to mock, cache or proxy every request core makes.
8
+ */
9
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
10
+
11
+ /**
12
+ * Resolves the global `fetch` at call time rather than capturing it on import,
13
+ * so a host that patches the global afterwards is still picked up.
14
+ */
15
+ const defaultFetch: FetchLike = (input, init) => fetch(input, init);
16
+
17
+ /**
18
+ * A response, parsed.
19
+ * A non-2xx is a value here rather than a throw:
20
+ * the retryer reads the body of a rejected request to tell a spent token from a rate limit.
21
+ */
22
+ interface HttpResponse<TData> {
23
+ status: number;
24
+ /** Reason phrase, which HTTP/2 omits — only ever used to annotate an error. */
25
+ statusText: string;
26
+ data: TData;
27
+ }
28
+
29
+ /** What the retryer hands a fetcher on each attempt. */
30
+ interface FetcherContext {
31
+ /** Transport to send the request with. */
32
+ fetch: FetchLike;
33
+ /** Attempt index, from 0. Tests read it to fake a rate limit on the first token. */
34
+ retries: number;
35
+ }
36
+
37
+ /**
38
+ * @param fetchImpl Transport to send the request with.
39
+ * @param url Absolute URL to request.
40
+ * @param init Request init forwarded to the transport.
41
+ * @returns The response; a JSON body parsed, anything else left as text.
42
+ */
43
+ const httpRequest = async <TData>(
44
+ fetchImpl: FetchLike,
45
+ url: string,
46
+ init?: RequestInit,
47
+ ): Promise<HttpResponse<TData>> => {
48
+ const response = await fetchImpl(url, init);
49
+ const body = await response.text();
50
+
51
+ let data: unknown;
52
+ try {
53
+ data = JSON.parse(body);
54
+ } catch {
55
+ // an empty or non-JSON body reads as text, so callers still get a defined `data`
56
+ data = body;
57
+ }
58
+
59
+ return {
60
+ status: response.status,
61
+ statusText: response.statusText,
62
+ data: data as TData,
63
+ };
64
+ };
65
+
66
+ /** Response of a GraphQL call: the envelope the GitHub API wraps results in. */
67
+ type GraphQLResponse<TResult> = HttpResponse<{
68
+ data: TResult;
69
+ errors?: Array<{ type?: string; message?: string }>;
70
+ }>;
71
+
72
+ /**
73
+ * @param document Generated query document.
74
+ * @param scheme `Authorization` scheme for the token.
75
+ * @returns A fetcher `retryer` can drive.
76
+ */
77
+ const createGraphQLFetcher = <TResult, TVariables>(
78
+ document: GraphQLDocument<TResult, TVariables>,
79
+ scheme: 'bearer' | 'token',
80
+ ) => {
81
+ return (
82
+ variables: TVariables,
83
+ token: string,
84
+ { fetch }: FetcherContext,
85
+ ): Promise<GraphQLResponse<TResult>> => {
86
+ return httpRequest(fetch, GITHUB_GRAPHQL_API, {
87
+ method: 'POST',
88
+ headers: {
89
+ Authorization: `${scheme} ${token}`,
90
+ 'Content-Type': 'application/json',
91
+ },
92
+ body: JSON.stringify({ query: document.text, variables }),
93
+ });
94
+ };
95
+ };
96
+
97
+ export { createGraphQLFetcher, defaultFetch, httpRequest };
98
+ export type { FetcherContext, FetchLike, GraphQLResponse, HttpResponse };
@@ -0,0 +1,80 @@
1
+ /*
2
+ The icons in this file are based on https://github.com/primer/octicons which is released under the MIT license:
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2026 GitHub Inc.
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+ */
26
+
27
+ const icons = {
28
+ star: `<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>`,
29
+ commits: `<path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"/>`,
30
+ prs: `<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>`,
31
+ prs_merged: `<path fill-rule="evenodd" d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z" />`,
32
+ prs_merged_percentage: `<path fill-rule="evenodd" d="M13.442 2.558a.625.625 0 0 1 0 .884l-10 10a.625.625 0 1 1-.884-.884l10-10a.625.625 0 0 1 .884 0zM4.5 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zm7 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z" />`,
33
+ issues: `<path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/>`,
34
+ icon: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
35
+ contribs: `<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>`,
36
+ fork: `<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path>`,
37
+ reviews: `<path fill-rule="evenodd" d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.677 1.367-1.931 2.637-3.022C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"/>`,
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
+ 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
+ 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"/>`,
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" />`,
43
+ };
44
+
45
+ /**
46
+ * Get rank icon
47
+ *
48
+ * @param rankIcon - The rank icon type.
49
+ * @param rankLevel - The rank level.
50
+ * @param percentile - The rank percentile.
51
+ * @returns The SVG code of the rank icon
52
+ */
53
+ const rankIcon = (rankIcon: string, rankLevel: string, percentile: number): string => {
54
+ switch (rankIcon) {
55
+ case 'github':
56
+ return `
57
+ <svg x="-38" y="-30" height="66" width="66" aria-hidden="true" viewBox="0 0 16 16" version="1.1" data-view-component="true" data-testid="github-rank-icon">
58
+ <path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
59
+ </svg>
60
+ `;
61
+ case 'percentile':
62
+ return `
63
+ <text x="-5" y="-12" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="percentile-top-header" class="rank-percentile-header">
64
+ Top
65
+ </text>
66
+ <text x="-5" y="12" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="percentile-rank-value" class="rank-percentile-text">
67
+ ${percentile.toFixed(1)}%
68
+ </text>
69
+ `;
70
+ case 'default':
71
+ default:
72
+ return `
73
+ <text x="-5" y="3" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="level-rank-icon">
74
+ ${rankLevel}
75
+ </text>
76
+ `;
77
+ }
78
+ };
79
+
80
+ export { icons, rankIcon };