@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
package/package.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "name": "@stats-forge/github-stats-forge-core",
3
+ "version": "0.0.1",
4
+ "description": "Dynamically generate stats for your GitHub readme",
5
+ "keywords": [
6
+ "card-generator",
7
+ "cards",
8
+ "github-stats",
9
+ "github-stats-forge",
10
+ "readme-stats",
11
+ "stats-forge"
12
+ ],
13
+ "homepage": "https://github.com/stats-forge/github-stats-forge",
14
+ "bugs": {
15
+ "url": "https://github.com/stats-forge/github-stats-forge/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": {
19
+ "name": "Anurag Hazra",
20
+ "url": "https://github.com/anuraghazra/"
21
+ },
22
+ "contributors": [
23
+ {
24
+ "name": "Rick Staa",
25
+ "url": "https://github.com/rickstaa"
26
+ },
27
+ {
28
+ "name": "Alexandr Garbuzov",
29
+ "url": "https://github.com/qwerty541"
30
+ },
31
+ {
32
+ "name": "Abhijit Gupta",
33
+ "url": "https://github.com/avgupta456"
34
+ },
35
+ {
36
+ "name": "Marco Pasqualetti",
37
+ "url": "https://github.com/marcalexiei"
38
+ },
39
+ {
40
+ "name": "martin-mfg",
41
+ "url": "https://github.com/martin-mfg"
42
+ }
43
+ ],
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/stats-forge/github-stats-forge.git",
47
+ "directory": "packages/core"
48
+ },
49
+ "files": [
50
+ "build",
51
+ "src",
52
+ "README.md"
53
+ ],
54
+ "type": "module",
55
+ "main": "./build/index.js",
56
+ "exports": {
57
+ ".": {
58
+ "@stats/source": "./src/index.ts",
59
+ "default": "./build/index.js"
60
+ },
61
+ "./api": {
62
+ "@stats/source": "./src/api/index.ts",
63
+ "default": "./build/api/index.js"
64
+ },
65
+ "./cards": {
66
+ "@stats/source": "./src/cards/index.ts",
67
+ "default": "./build/cards/index.js"
68
+ },
69
+ "./fetchers": {
70
+ "@stats/source": "./src/fetchers/index.ts",
71
+ "default": "./build/fetchers/index.js"
72
+ },
73
+ "./themes": {
74
+ "@stats/source": "./src/themes/index.ts",
75
+ "default": "./build/themes/index.js"
76
+ }
77
+ },
78
+ "publishConfig": {
79
+ "access": "public"
80
+ },
81
+ "dependencies": {
82
+ "emoji-name-map": "^2.0.3",
83
+ "github-username-regex": "^1.0.0",
84
+ "zod": "^4.4.3"
85
+ },
86
+ "devDependencies": {
87
+ "@graphql-codegen/core": "6.2.0",
88
+ "@graphql-codegen/typescript": "6.1.0",
89
+ "@graphql-codegen/typescript-operations": "6.1.2",
90
+ "@octokit/graphql-schema": "15.26.1",
91
+ "@testing-library/dom": "10.4.1",
92
+ "@testing-library/jest-dom": "6.9.1",
93
+ "@uppercod/css-to-object": "1.1.1",
94
+ "graphql": "16.11.0",
95
+ "js-yaml": "5.2.1",
96
+ "jsdom": "29.1.1",
97
+ "vitest": "4.1.10"
98
+ },
99
+ "engines": {
100
+ "node": ">=24"
101
+ },
102
+ "scripts": {
103
+ "build": "tsc -p tsconfig.build.json",
104
+ "test": "vitest",
105
+ "test:update:snapshot": "vitest -u",
106
+ "test:e2e": "vitest --config vitest.config.e2e.ts",
107
+ "bench": "vitest bench --run --config vitest.config.bench.ts",
108
+ "lint": "eslint",
109
+ "typecheck": "tsc -p tsconfig.typecheck.json",
110
+ "generate-graphql-types": "node scripts/generate-graphql-types",
111
+ "check-graphql-types": "node scripts/generate-graphql-types --check",
112
+ "generate-language-colors": "node scripts/generate-language-colors"
113
+ }
114
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `emoji-name-map` ships no type definitions and exposes no `exports` map, so it
3
+ * cannot be resolved under `nodenext`. Declare the minimal surface used here.
4
+ */
5
+ declare module 'emoji-name-map' {
6
+ const emojiNameMap: {
7
+ get(name: string): string | undefined;
8
+ };
9
+ export default emojiNameMap;
10
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'github-username-regex' {
2
+ const githubUsernameRegex: RegExp;
3
+ export default githubUsernameRegex;
4
+ }
@@ -0,0 +1,69 @@
1
+ import type { ColorParams } from '../common/color.js';
2
+ import type { ErrorCode } from '../common/error.js';
3
+ import { CardError } from '../common/error.js';
4
+ import { renderError } from '../common/render.js';
5
+
6
+ /**
7
+ * Why a card could not be rendered, in a form a host can act on.
8
+ * The same failure is also drawn onto the result's `content`,
9
+ * so a host never has to read the SVG to find out what happened.
10
+ */
11
+ export interface ApiError {
12
+ /** Why it failed. */
13
+ code: ErrorCode;
14
+ /** First line of the error card. */
15
+ message: string;
16
+ /** Second line of the error card, when the code has more to say. */
17
+ secondaryMessage: string | undefined;
18
+ /**
19
+ * The first param at fault, when the failure names one.
20
+ * `missing_param` names every missing param in `message`, and not every
21
+ * `invalid_param` comes from a single param, so this can be absent.
22
+ */
23
+ param: string | undefined;
24
+ }
25
+
26
+ /** What every api handler returns: a rendered card, or a rendered error. */
27
+ export type ApiResult =
28
+ | { status: 'success'; content: string }
29
+ | {
30
+ status: 'error';
31
+ /** Whether repeating the request could produce a different answer. */
32
+ retryable: boolean;
33
+ error: ApiError;
34
+ /** The error, drawn as a card. */
35
+ content: string;
36
+ };
37
+
38
+ /**
39
+ * Turns anything a handler threw into the one shape it answers with.
40
+ *
41
+ * @param err The failure.
42
+ * Anything that is not a `CardError` is treated as upstream's.
43
+ * @param renderOptions Colors the error card is drawn with;
44
+ * omitted when it was a color that was rejected.
45
+ * @returns The rendered error.
46
+ */
47
+ export const errorResult = (err: unknown, renderOptions?: ColorParams): ApiResult => {
48
+ const error = CardError.from(err);
49
+
50
+ return {
51
+ status: 'error',
52
+ retryable: error.retryable,
53
+ error: {
54
+ code: error.code,
55
+ message: error.message,
56
+ secondaryMessage: error.secondaryMessage,
57
+ param: error.param,
58
+ },
59
+ content: renderError({
60
+ message: error.message,
61
+ secondaryMessage: error.secondaryMessage,
62
+ renderOptions: {
63
+ ...renderOptions,
64
+ // A missing param is the caller's to fix, so the card links its docs instead.
65
+ show_repo_link: error.code !== 'missing_param',
66
+ },
67
+ }),
68
+ };
69
+ };
@@ -0,0 +1,66 @@
1
+ import * as z from 'zod/mini';
2
+
3
+ import { renderGistCard } from '../cards/gist.js';
4
+ import type { CardConfig } from '../common/config.js';
5
+ import { fetchGist } from '../fetchers/gist.js';
6
+
7
+ import type { ApiResult } from './api-result.js';
8
+ import { errorResult } from './api-result.js';
9
+ import type { ApiQuery } from './params.js';
10
+ import { booleanParam, numberParam, parseColorParams, parseParams, safeParam } from './params.js';
11
+
12
+ /** What the gist endpoint accepts, on top of the shared color params. */
13
+ const gistQuery = z.object({
14
+ id: safeParam,
15
+ border_radius: numberParam,
16
+ show_owner: booleanParam,
17
+ browser_rendering: booleanParam,
18
+ hide_border: booleanParam,
19
+ });
20
+
21
+ /** The query this endpoint accepts, checked against the schema above. */
22
+ type GistApiQuery = ApiQuery<typeof gistQuery>;
23
+
24
+ /**
25
+ * Render the gist card for a set of query params.
26
+ *
27
+ * @param query Raw query params, plus any of the shared color params.
28
+ * @param query.id GitHub gist ID.
29
+ * @param query.border_radius Card border radius.
30
+ * @param query.show_owner Whether to show the gist owner.
31
+ * @param query.browser_rendering Whether the browser wraps the description text.
32
+ * @param query.hide_border Whether to hide the card border.
33
+ * @param config Deployment config supplying the PAT pool.
34
+ * @returns The rendered card, or a rendered error.
35
+ */
36
+ export const gist = async (query: GistApiQuery, config: CardConfig): Promise<ApiResult> => {
37
+ let colors;
38
+ try {
39
+ colors = parseColorParams(query);
40
+ } catch (err) {
41
+ // A rejected color cannot be used to draw its own error card.
42
+ return errorResult(err);
43
+ }
44
+
45
+ try {
46
+ const { id, border_radius, show_owner, browser_rendering, hide_border } = parseParams(
47
+ gistQuery,
48
+ query,
49
+ );
50
+
51
+ const gistData = await fetchGist({ id }, config);
52
+
53
+ return {
54
+ status: 'success',
55
+ content: renderGistCard(gistData, {
56
+ ...colors,
57
+ border_radius,
58
+ show_owner,
59
+ browser_rendering,
60
+ hide_border,
61
+ }),
62
+ };
63
+ } catch (err) {
64
+ return errorResult(err, colors);
65
+ }
66
+ };
@@ -0,0 +1,16 @@
1
+ export { gist } from './gist.js';
2
+ export { pin } from './pin.js';
3
+ export { stats } from './stats.js';
4
+ export { topLangs } from './top-langs.js';
5
+ export { wakatime } from './wakatime.js';
6
+
7
+ export type { ApiError, ApiResult } from './api-result.js';
8
+
9
+ // every handler but `wakatime` needs the PAT pool, and that one still needs the transport.
10
+ export { CardConfig } from '../common/config.js';
11
+ export type { CardConfigInit, PersonalAccessToken } from '../common/config.js';
12
+ export type { FetchLike } from '../common/http.js';
13
+ export type { ErrorCode } from '../common/error.js';
14
+
15
+ export { themes } from '../themes/index.js';
16
+ export type { ThemeName } from '../themes/index.js';
@@ -0,0 +1,221 @@
1
+ import * as z from 'zod/mini';
2
+
3
+ import type { ColorParams } from '../common/color.js';
4
+ import { COLOR_PARAM_KEYS, THEME_PARAM_KEYS, isValidColorInput } from '../common/color.js';
5
+ import { CardError } from '../common/error.js';
6
+ import { parseArray, parseBoolean } from '../common/ops.js';
7
+ import { isLocaleAvailable } from '../translations.js';
8
+
9
+ /**
10
+ * @file The api layer is the trust boundary:
11
+ * a query string arrives as strings, and each endpoint declares what it accepts as a schema over them.
12
+ * Parsing happens here once;
13
+ * the render functions are handed typed values and keep their own defaults.
14
+ *
15
+ * `zod/mini` rather than `zod`:
16
+ * this package ships to the browser, and the functional API tree-shakes down to the checks used below.
17
+ */
18
+
19
+ /** Every param arrives as a string, or not at all. */
20
+ const rawParam = z.optional(z.string());
21
+
22
+ /** Characters a username, repository, owner or gist id may contain. */
23
+ const SAFE_PATTERN = /^[-\w/.,]+$/;
24
+
25
+ /** What a check rejected a param for. */
26
+ type Rejection = 'number' | 'year' | 'unsafe' | 'locale' | 'enum' | 'color';
27
+
28
+ /**
29
+ * Every rejection the api can put on an error card, in one place.
30
+ * The param comes from the issue's own path, so no schema repeats its own name.
31
+ */
32
+ const REJECTION_MESSAGES: Record<Rejection, (param: string) => string> = {
33
+ number: (param) => `Invalid number input for parameter "${param}"`,
34
+ year: (param) => `Invalid number input for parameter "${param}"`,
35
+ unsafe: (param) => `Parameter "${param}" contains unsafe characters`,
36
+ locale: () => 'Locale not found',
37
+ enum: (param) => `Incorrect ${param} input`,
38
+ color: (param) => `Invalid color input for parameter "${param}"`,
39
+ };
40
+
41
+ /**
42
+ * A check that words its own rejection:
43
+ * the message is built from the kind and the param's path rather than passed in.
44
+ *
45
+ * @param kind What the check rejects the param for.
46
+ * @param passes Whether a value is acceptable.
47
+ * @returns The check, ready for `.check()`.
48
+ */
49
+ const rejects = (kind: Rejection, passes: (value: string) => boolean) =>
50
+ z.refine((value: unknown) => typeof value !== 'string' || passes(value), {
51
+ error: (issue) => REJECTION_MESSAGES[kind](String(issue.path?.[0] ?? '')),
52
+ });
53
+
54
+ /**
55
+ * `?x=true` / `?x=false`.
56
+ * Anything else is `undefined`, which leaves the card's own default in place.
57
+ */
58
+ const booleanParam = z.pipe(
59
+ rawParam,
60
+ // Narrowed: `parseBoolean` also takes booleans, which a query string cannot hold.
61
+ z.transform((value: string | undefined) => parseBoolean(value)),
62
+ );
63
+
64
+ /** Comma-separated values, empty when the param is absent. */
65
+ const listParam = z.pipe(rawParam, z.transform(parseArray));
66
+
67
+ /**
68
+ * A number the card cannot fall back from, so a malformed one is rejected here:
69
+ * a render-time guard would throw into the generic catch and read as a temporary error.
70
+ *
71
+ * `parseFloat`, matching the coercion `Card` performs internally, so `?border_radius=10px` still renders `rx="10"`.
72
+ * Yields the parsed number, or `undefined` when the param is absent.
73
+ */
74
+ const numberParam: z.ZodMiniType<number | undefined, string | undefined> = z.pipe(
75
+ rawParam.check(rejects('number', (value) => Number.isFinite(parseFloat(value)))),
76
+ z.transform((value) => (value === undefined ? undefined : parseFloat(value))),
77
+ );
78
+
79
+ /**
80
+ * A number the card already falls back from — widths, counts, line heights:
81
+ * `NaN` reaches the renderer, which answers it with its own default.
82
+ */
83
+ const looseIntParam = z.pipe(
84
+ rawParam,
85
+ z.transform((value) => (value === undefined ? undefined : parseInt(value, 10))),
86
+ );
87
+
88
+ /**
89
+ * A four-digit year.
90
+ * Anything else builds a `DateTime` GitHub rejects, so it is a permanent error here rather than a failed request later.
91
+ * Yields the year, or `undefined` when the param is absent.
92
+ */
93
+ const yearParam: z.ZodMiniType<number | undefined, string | undefined> = z.pipe(
94
+ rawParam.check(rejects('year', (value) => /^\d{4}$/.test(value))),
95
+ z.transform((value) => (value === undefined ? undefined : Number(value))),
96
+ );
97
+
98
+ /**
99
+ * An id the fetchers put in a URL.
100
+ * Rejected before any request is made, and yielded unchanged otherwise.
101
+ */
102
+ const safeParam: z.ZodMiniType<string | undefined, string | undefined> = rawParam.check(
103
+ rejects('unsafe', (value) => !value || SAFE_PATTERN.test(value)),
104
+ );
105
+
106
+ /**
107
+ * A comma-separated list of ids, checked before it is split.
108
+ * The safe pattern allows the commas, so one check covers the whole list.
109
+ * Yields the split values, empty when the param is absent.
110
+ */
111
+ const safeListParam: z.ZodMiniType<Array<string>, string | undefined> = z.pipe(
112
+ safeParam,
113
+ z.transform(parseArray),
114
+ );
115
+
116
+ /**
117
+ * A locale the cards have translations for.
118
+ * One wording for every endpoint:
119
+ * the message names the param, as the number and color rejections do.
120
+ */
121
+ const localeParam = z.pipe(
122
+ rawParam.check(rejects('locale', (value) => !value || isLocaleAvailable(value))),
123
+ z.transform((value) => value?.toLowerCase()),
124
+ );
125
+
126
+ /**
127
+ * A param the card only renders as one of a fixed set of values.
128
+ *
129
+ * @param values The accepted values, as the card that renders them declares them.
130
+ * @returns Schema yielding one of `values`, or `undefined` when absent.
131
+ */
132
+ const enumParam = <const T extends ReadonlyArray<string>>(
133
+ values: T,
134
+ ): z.ZodMiniType<T[number] | undefined, string | undefined> =>
135
+ rawParam.check(rejects('enum', (value) => (values as ReadonlyArray<string>).includes(value)));
136
+
137
+ /**
138
+ * Every color param an endpoint accepts, validated and picked in one pass.
139
+ *
140
+ * Theme params are `rawParam`:
141
+ * they name a theme, and an unknown name falls back to the default rather than being an error.
142
+ */
143
+ const colorParamsSchema = z.object(
144
+ Object.fromEntries(
145
+ COLOR_PARAM_KEYS.map((key) => [
146
+ key,
147
+ THEME_PARAM_KEYS.includes(key)
148
+ ? rawParam
149
+ : rawParam.check(rejects('color', isValidColorInput)),
150
+ ]),
151
+ ),
152
+ );
153
+
154
+ /**
155
+ * The query an endpoint accepts, derived from its own schema.
156
+ * Every param is optional and every value a string, which is all a query string can carry;
157
+ * naming it lets a consumer typecheck the object it builds.
158
+ */
159
+ type ApiQuery<TSchema extends z.ZodMiniType> = Partial<z.input<TSchema>> & ColorParams;
160
+
161
+ /**
162
+ * Turns a rejection back into the error the api answers with.
163
+ * Every rejection is the query's fault, so they share one code
164
+ * and differ only in the message the check already worded.
165
+ *
166
+ * @param error What the schema rejected.
167
+ * @returns The failure, ready to render.
168
+ */
169
+ const toCardError = (error: z.core.$ZodError): CardError => {
170
+ const issue = error.issues[0];
171
+ return CardError.invalidParam(String(issue?.path[0] ?? ''), issue?.message ?? 'Invalid input');
172
+ };
173
+
174
+ /**
175
+ * Runs a query through an endpoint's schema.
176
+ *
177
+ * Only the first rejection is reported:
178
+ * the error card has room for one line.
179
+ *
180
+ * @param schema The endpoint's schema.
181
+ * @param query Raw query params.
182
+ * @returns The parsed params.
183
+ * @throws {CardError} When the schema rejects a param.
184
+ */
185
+ const parseParams = <TSchema extends z.ZodMiniType>(
186
+ schema: TSchema,
187
+ query: unknown,
188
+ ): z.output<TSchema> => {
189
+ const result = z.safeParse(schema, query);
190
+ if (!result.success) {
191
+ throw toCardError(result.error);
192
+ }
193
+ return result.data;
194
+ };
195
+
196
+ /**
197
+ * The color params, validated.
198
+ * Split from the endpoint's own schema because a rejected color cannot then be used to render its own error card.
199
+ *
200
+ * @param query Raw query params.
201
+ * @returns The color params.
202
+ * @throws {CardError} When a param does not hold a color or a gradient.
203
+ */
204
+ const parseColorParams = (query: unknown): ColorParams => parseParams(colorParamsSchema, query);
205
+
206
+ export type { ApiQuery };
207
+
208
+ export {
209
+ booleanParam,
210
+ enumParam,
211
+ listParam,
212
+ localeParam,
213
+ looseIntParam,
214
+ numberParam,
215
+ parseColorParams,
216
+ parseParams,
217
+ rawParam,
218
+ safeListParam,
219
+ safeParam,
220
+ yearParam,
221
+ };
package/src/api/pin.ts ADDED
@@ -0,0 +1,126 @@
1
+ import * as z from 'zod/mini';
2
+
3
+ import { renderRepoCard } from '../cards/repo.js';
4
+ import type { CardConfig } from '../common/config.js';
5
+ import { fetchRepo } from '../fetchers/repo.js';
6
+
7
+ import type { ApiResult } from './api-result.js';
8
+ import { errorResult } from './api-result.js';
9
+ import type { ApiQuery } from './params.js';
10
+ import {
11
+ booleanParam,
12
+ listParam,
13
+ localeParam,
14
+ looseIntParam,
15
+ numberParam,
16
+ parseColorParams,
17
+ parseParams,
18
+ rawParam,
19
+ safeParam,
20
+ } from './params.js';
21
+
22
+ /** What the pin endpoint accepts, on top of the shared color params. */
23
+ const pinQuery = z.object({
24
+ username: safeParam,
25
+ repo: safeParam,
26
+ hide_border: booleanParam,
27
+ card_width: looseIntParam,
28
+ show_owner: booleanParam,
29
+ browser_rendering: booleanParam,
30
+ show: listParam,
31
+ show_icons: booleanParam,
32
+ number_format: rawParam,
33
+ text_bold: booleanParam,
34
+ line_height: rawParam,
35
+ locale: localeParam,
36
+ border_radius: numberParam,
37
+ description_lines_count: looseIntParam,
38
+ });
39
+
40
+ /** The query this endpoint accepts, checked against the schema above. */
41
+ type PinApiQuery = ApiQuery<typeof pinQuery>;
42
+
43
+ /**
44
+ * Render the repository card for a set of query params.
45
+ *
46
+ * @param query Raw query params, plus any of the shared color params.
47
+ * @param query.username GitHub username the repository belongs to.
48
+ * @param query.repo Repository name.
49
+ * @param query.hide_border Whether to hide the card border.
50
+ * @param query.card_width Card width.
51
+ * @param query.show_owner Whether to show the repository owner.
52
+ * @param query.browser_rendering Whether the browser wraps the description text.
53
+ * @param query.show Comma-separated extra stats to show.
54
+ * @param query.show_icons Whether to show the stat icons.
55
+ * @param query.number_format How numbers are abbreviated.
56
+ * @param query.text_bold Whether the stat values are bold.
57
+ * @param query.line_height Line height between the stats.
58
+ * @param query.locale Language the card is rendered in.
59
+ * @param query.border_radius Card border radius.
60
+ * @param query.description_lines_count Lines the description is wrapped to.
61
+ * @param config Deployment config supplying the PAT pool.
62
+ * @returns The rendered card, or a rendered error.
63
+ */
64
+ export const pin = async (query: PinApiQuery, config: CardConfig): Promise<ApiResult> => {
65
+ let colors;
66
+ try {
67
+ colors = parseColorParams(query);
68
+ } catch (err) {
69
+ // A rejected color cannot be used to draw its own error card.
70
+ return errorResult(err);
71
+ }
72
+
73
+ try {
74
+ const {
75
+ username,
76
+ repo,
77
+ hide_border,
78
+ card_width,
79
+ show_owner,
80
+ browser_rendering,
81
+ show,
82
+ show_icons,
83
+ number_format,
84
+ text_bold,
85
+ line_height,
86
+ locale,
87
+ border_radius,
88
+ description_lines_count,
89
+ } = parseParams(pinQuery, query);
90
+
91
+ const repoData = await fetchRepo(
92
+ {
93
+ username,
94
+ reponame: repo,
95
+ include_prs_authored: show.includes('prs_authored'),
96
+ include_prs_commented: show.includes('prs_commented'),
97
+ include_prs_reviewed: show.includes('prs_reviewed'),
98
+ include_issues_authored: show.includes('issues_authored'),
99
+ include_issues_commented: show.includes('issues_commented'),
100
+ },
101
+ config,
102
+ );
103
+
104
+ return {
105
+ status: 'success',
106
+ content: renderRepoCard(repoData, {
107
+ ...colors,
108
+ hide_border,
109
+ border_radius,
110
+ card_width_input: card_width,
111
+ show_owner,
112
+ browser_rendering,
113
+ show,
114
+ show_icons,
115
+ number_format,
116
+ text_bold,
117
+ line_height,
118
+ username,
119
+ locale,
120
+ description_lines_count,
121
+ }),
122
+ };
123
+ } catch (err) {
124
+ return errorResult(err, colors);
125
+ }
126
+ };