@dytsou/github-readme-stats 1.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/api/gist.js +98 -0
  3. package/api/index.js +146 -0
  4. package/api/pin.js +114 -0
  5. package/api/status/pat-info.js +193 -0
  6. package/api/status/up.js +129 -0
  7. package/api/top-langs.js +163 -0
  8. package/api/wakatime.js +129 -0
  9. package/package.json +102 -0
  10. package/readme.md +412 -0
  11. package/src/calculateRank.js +87 -0
  12. package/src/cards/gist.js +151 -0
  13. package/src/cards/index.js +4 -0
  14. package/src/cards/repo.js +199 -0
  15. package/src/cards/stats.js +607 -0
  16. package/src/cards/top-languages.js +968 -0
  17. package/src/cards/types.d.ts +67 -0
  18. package/src/cards/wakatime.js +482 -0
  19. package/src/common/Card.js +294 -0
  20. package/src/common/I18n.js +41 -0
  21. package/src/common/access.js +69 -0
  22. package/src/common/api-utils.js +221 -0
  23. package/src/common/blacklist.js +10 -0
  24. package/src/common/cache.js +153 -0
  25. package/src/common/color.js +194 -0
  26. package/src/common/envs.js +15 -0
  27. package/src/common/error.js +84 -0
  28. package/src/common/fmt.js +90 -0
  29. package/src/common/html.js +43 -0
  30. package/src/common/http.js +24 -0
  31. package/src/common/icons.js +63 -0
  32. package/src/common/index.js +13 -0
  33. package/src/common/languageColors.json +651 -0
  34. package/src/common/log.js +14 -0
  35. package/src/common/ops.js +124 -0
  36. package/src/common/render.js +261 -0
  37. package/src/common/retryer.js +97 -0
  38. package/src/common/worker-adapter.js +148 -0
  39. package/src/common/worker-env.js +48 -0
  40. package/src/fetchers/gist.js +114 -0
  41. package/src/fetchers/repo.js +118 -0
  42. package/src/fetchers/stats.js +350 -0
  43. package/src/fetchers/top-languages.js +192 -0
  44. package/src/fetchers/types.d.ts +118 -0
  45. package/src/fetchers/wakatime.js +109 -0
  46. package/src/index.js +2 -0
  47. package/src/translations.js +1105 -0
  48. package/src/worker.ts +116 -0
  49. package/themes/README.md +229 -0
  50. package/themes/index.js +467 -0
@@ -0,0 +1,163 @@
1
+ // @ts-check
2
+
3
+ import { renderTopLanguages } from "../src/cards/top-languages.js";
4
+ import { guardAccess } from "../src/common/access.js";
5
+ import {
6
+ createValidatedColorOptions,
7
+ handleApiError,
8
+ sendValidationError,
9
+ setSvgContentType,
10
+ parseNumericParam,
11
+ } from "../src/common/api-utils.js";
12
+ import {
13
+ CACHE_TTL,
14
+ resolveCacheSeconds,
15
+ setCacheHeaders,
16
+ } from "../src/common/cache.js";
17
+ import { parseArray, parseBoolean } from "../src/common/ops.js";
18
+ import { fetchTopLanguages } from "../src/fetchers/top-languages.js";
19
+ import { isLocaleAvailable } from "../src/translations.js";
20
+
21
+ /** @type {readonly string[]} */
22
+ const VALID_LAYOUTS = ["compact", "normal", "donut", "donut-vertical", "pie"];
23
+
24
+ /** @type {readonly string[]} */
25
+ const VALID_STATS_FORMATS = ["bytes", "percentages"];
26
+
27
+ // @ts-ignore
28
+ export default async (req, res) => {
29
+ const {
30
+ username,
31
+ hide,
32
+ hide_title,
33
+ hide_border,
34
+ card_width,
35
+ title_color,
36
+ text_color,
37
+ bg_color,
38
+ theme,
39
+ cache_seconds,
40
+ layout,
41
+ langs_count,
42
+ exclude_repo,
43
+ size_weight,
44
+ count_weight,
45
+ custom_title,
46
+ locale: rawLocale,
47
+ border_radius,
48
+ border_color,
49
+ disable_animations,
50
+ hide_progress,
51
+ stats_format,
52
+ } = req.query;
53
+
54
+ // Only allow supported locales - validate and sanitize to prevent XSS
55
+ const locale =
56
+ typeof rawLocale === "string" && isLocaleAvailable(rawLocale)
57
+ ? rawLocale.toLowerCase()
58
+ : undefined;
59
+
60
+ // Create validated color options once for reuse
61
+ const colorOptions = createValidatedColorOptions({
62
+ title_color,
63
+ text_color,
64
+ bg_color,
65
+ border_color,
66
+ theme,
67
+ });
68
+
69
+ // Validate username is provided
70
+ if (!username) {
71
+ return sendValidationError({
72
+ res,
73
+ message: "Missing username parameter",
74
+ secondaryMessage: "Please provide a username",
75
+ colorOptions,
76
+ });
77
+ }
78
+
79
+ // Set Content-Type early for Camo CDN compatibility
80
+ setSvgContentType(res);
81
+
82
+ const access = guardAccess({
83
+ res,
84
+ id: username,
85
+ type: "username",
86
+ colors: colorOptions,
87
+ });
88
+ if (!access.isPassed) {
89
+ return access.result;
90
+ }
91
+
92
+ // Validate layout parameter
93
+ if (
94
+ layout !== undefined &&
95
+ (typeof layout !== "string" || !VALID_LAYOUTS.includes(layout))
96
+ ) {
97
+ return sendValidationError({
98
+ res,
99
+ message: "Something went wrong",
100
+ secondaryMessage: "Incorrect layout input",
101
+ colorOptions,
102
+ });
103
+ }
104
+
105
+ // Validate stats_format parameter
106
+ if (
107
+ stats_format !== undefined &&
108
+ (typeof stats_format !== "string" ||
109
+ !VALID_STATS_FORMATS.includes(stats_format))
110
+ ) {
111
+ return sendValidationError({
112
+ res,
113
+ message: "Something went wrong",
114
+ secondaryMessage: "Incorrect stats_format input",
115
+ colorOptions,
116
+ });
117
+ }
118
+
119
+ try {
120
+ const topLangs = await fetchTopLanguages(
121
+ username,
122
+ parseArray(exclude_repo),
123
+ size_weight,
124
+ count_weight,
125
+ );
126
+ const cacheSeconds = resolveCacheSeconds({
127
+ requested: parseInt(cache_seconds, 10),
128
+ def: CACHE_TTL.TOP_LANGS_CARD.DEFAULT,
129
+ min: CACHE_TTL.TOP_LANGS_CARD.MIN,
130
+ max: CACHE_TTL.TOP_LANGS_CARD.MAX,
131
+ });
132
+
133
+ setCacheHeaders(res, cacheSeconds);
134
+
135
+ return res.send(
136
+ renderTopLanguages(topLangs, {
137
+ // Validate custom_title is a string (prevents array from duplicate query params)
138
+ // Card.js handles HTML encoding internally
139
+ custom_title:
140
+ typeof custom_title === "string" ? custom_title : undefined,
141
+ hide_title: parseBoolean(hide_title),
142
+ hide_border: parseBoolean(hide_border),
143
+ card_width: parseInt(card_width, 10),
144
+ hide: parseArray(hide),
145
+ title_color: colorOptions.title_color,
146
+ text_color: colorOptions.text_color,
147
+ bg_color: colorOptions.bg_color,
148
+ // @ts-ignore - validateTheme returns a validated theme name
149
+ theme: colorOptions.theme,
150
+ layout,
151
+ langs_count,
152
+ border_radius: parseNumericParam(border_radius, undefined, 0, 50),
153
+ border_color: colorOptions.border_color,
154
+ locale,
155
+ disable_animations: parseBoolean(disable_animations),
156
+ hide_progress: parseBoolean(hide_progress),
157
+ stats_format,
158
+ }),
159
+ );
160
+ } catch (err) {
161
+ return handleApiError({ res, error: err, colorOptions });
162
+ }
163
+ };
@@ -0,0 +1,129 @@
1
+ // @ts-check
2
+
3
+ import { renderWakatimeCard } from "../src/cards/wakatime.js";
4
+ import { guardAccess } from "../src/common/access.js";
5
+ import {
6
+ createValidatedColorOptions,
7
+ handleApiError,
8
+ setSvgContentType,
9
+ parseNumericParam,
10
+ } from "../src/common/api-utils.js";
11
+ import {
12
+ CACHE_TTL,
13
+ resolveCacheSeconds,
14
+ setCacheHeaders,
15
+ } from "../src/common/cache.js";
16
+ import { validateColor } from "../src/common/color.js";
17
+ import { parseArray, parseBoolean } from "../src/common/ops.js";
18
+ import { fetchWakatimeStats } from "../src/fetchers/wakatime.js";
19
+ import { isLocaleAvailable } from "../src/translations.js";
20
+
21
+ /** @type {number} */
22
+ const DEFAULT_BORDER_RADIUS = 4.5;
23
+
24
+ /** @type {number} */
25
+ const MAX_BORDER_RADIUS = 20;
26
+
27
+ // @ts-ignore
28
+ export default async (req, res) => {
29
+ const {
30
+ username,
31
+ title_color,
32
+ icon_color,
33
+ hide_border,
34
+ card_width,
35
+ line_height,
36
+ text_color,
37
+ bg_color,
38
+ theme,
39
+ cache_seconds,
40
+ hide_title,
41
+ hide_progress,
42
+ custom_title,
43
+ locale: rawLocale,
44
+ layout,
45
+ langs_count,
46
+ hide,
47
+ api_domain,
48
+ border_radius,
49
+ border_color,
50
+ display_format,
51
+ disable_animations,
52
+ } = req.query;
53
+
54
+ // Only allow supported locales - validate and sanitize to prevent XSS
55
+ const locale =
56
+ typeof rawLocale === "string" && isLocaleAvailable(rawLocale)
57
+ ? rawLocale.toLowerCase()
58
+ : undefined;
59
+
60
+ setSvgContentType(res);
61
+
62
+ // Create validated color options once for reuse
63
+ const colorOptions = createValidatedColorOptions({
64
+ title_color,
65
+ text_color,
66
+ bg_color,
67
+ border_color,
68
+ theme,
69
+ });
70
+
71
+ const access = guardAccess({
72
+ res,
73
+ id: username,
74
+ type: "wakatime",
75
+ colors: colorOptions,
76
+ });
77
+ if (!access.isPassed) {
78
+ return access.result;
79
+ }
80
+
81
+ try {
82
+ const stats = await fetchWakatimeStats({ username, api_domain });
83
+ const cacheSeconds = resolveCacheSeconds({
84
+ requested: parseInt(cache_seconds, 10),
85
+ def: CACHE_TTL.WAKATIME_CARD.DEFAULT,
86
+ min: CACHE_TTL.WAKATIME_CARD.MIN,
87
+ max: CACHE_TTL.WAKATIME_CARD.MAX,
88
+ });
89
+
90
+ setCacheHeaders(res, cacheSeconds);
91
+
92
+ return res.send(
93
+ renderWakatimeCard(stats, {
94
+ // Validate custom_title is a string (prevents array from duplicate query params)
95
+ // Card.js handles HTML encoding internally
96
+ custom_title:
97
+ typeof custom_title === "string" ? custom_title : undefined,
98
+ hide_title: parseBoolean(hide_title),
99
+ hide_border: parseBoolean(hide_border),
100
+ card_width: parseInt(card_width, 10),
101
+ hide: parseArray(hide),
102
+ line_height,
103
+ title_color: colorOptions.title_color,
104
+ icon_color: validateColor(icon_color),
105
+ text_color: colorOptions.text_color,
106
+ bg_color: colorOptions.bg_color,
107
+ // @ts-ignore - validateTheme ensures theme is valid ThemeNames
108
+ theme: colorOptions.theme,
109
+ hide_progress,
110
+ border_radius: parseNumericParam(
111
+ border_radius,
112
+ DEFAULT_BORDER_RADIUS,
113
+ 0,
114
+ MAX_BORDER_RADIUS,
115
+ ),
116
+ border_color: colorOptions.border_color,
117
+ locale,
118
+ layout,
119
+ langs_count,
120
+ display_format,
121
+ disable_animations: parseBoolean(disable_animations),
122
+ }),
123
+ );
124
+ } catch (err) {
125
+ // handleApiError sanitizes error messages via sanitizeErrorMessage()
126
+ // which replaces unsafe patterns containing user data with safe alternatives
127
+ return handleApiError({ res, error: err, colorOptions });
128
+ }
129
+ };
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "@dytsou/github-readme-stats",
3
+ "version": "1.0.1",
4
+ "description": "Dynamically generate stats for your GitHub readme",
5
+ "keywords": [
6
+ "github-readme-stats",
7
+ "readme-stats",
8
+ "cards",
9
+ "card-generator"
10
+ ],
11
+ "main": "src/index.js",
12
+ "type": "module",
13
+ "exports": {
14
+ ".": "./src/index.js",
15
+ "./worker": "./src/worker.ts"
16
+ },
17
+ "files": [
18
+ "src",
19
+ "api",
20
+ "themes",
21
+ "LICENSE",
22
+ "readme.md"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "homepage": "https://github.com/anuraghazra/github-readme-stats",
28
+ "bugs": {
29
+ "url": "https://github.com/anuraghazra/github-readme-stats/issues"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/anuraghazra/github-readme-stats.git"
34
+ },
35
+ "author": "Anurag Hazra",
36
+ "license": "MIT",
37
+ "devDependencies": {
38
+ "@actions/core": "^1.11.1",
39
+ "@actions/github": "^6.0.1",
40
+ "@eslint/eslintrc": "^3.3.1",
41
+ "@eslint/js": "^9.39.0",
42
+ "@testing-library/dom": "^10.4.1",
43
+ "@testing-library/jest-dom": "^6.9.1",
44
+ "@types/escape-html": "^1.0.4",
45
+ "@uppercod/css-to-object": "^1.1.1",
46
+ "@vitest/coverage-v8": "^4.0.15",
47
+ "@vitest/ui": "^4.0.15",
48
+ "axios-mock-adapter": "^2.1.0",
49
+ "color-contrast-checker": "^2.1.0",
50
+ "eslint": "^9.39.0",
51
+ "eslint-config-prettier": "^10.1.8",
52
+ "eslint-plugin-jsdoc": "^61.1.12",
53
+ "express": "^5.1.0",
54
+ "globals": "^16.5.0",
55
+ "happy-dom": "^20.0.11",
56
+ "hjson": "^3.2.2",
57
+ "husky": "^9.1.7",
58
+ "js-yaml": "^4.1.0",
59
+ "jsdom": "^27.2.0",
60
+ "lint-staged": "^16.2.6",
61
+ "lodash.snakecase": "^4.1.1",
62
+ "parse-diff": "^0.11.1",
63
+ "prettier": "^3.6.2",
64
+ "vitest": "^4.0.15"
65
+ },
66
+ "dependencies": {
67
+ "@hono/node-server": "^1.19.6",
68
+ "axios": "^1.13.1",
69
+ "dotenv": "^17.2.3",
70
+ "emoji-name-map": "^2.0.3",
71
+ "escape-html": "^1.0.3",
72
+ "github-username-regex": "^1.0.0",
73
+ "hono": "^4.10.7",
74
+ "word-wrap": "^1.2.5"
75
+ },
76
+ "lint-staged": {
77
+ "*.{js,css,md}": "prettier --write"
78
+ },
79
+ "engines": {
80
+ "node": ">=22"
81
+ },
82
+ "scripts": {
83
+ "test": "mkdir -p .coverage-tmp && vitest run --coverage",
84
+ "test:watch": "vitest watch",
85
+ "test:ui": "vitest --ui",
86
+ "test:update:snapshot": "vitest run -u",
87
+ "test:e2e": "vitest run --config vitest.e2e.config.js",
88
+ "theme-readme-gen": "node scripts/generate-theme-doc",
89
+ "preview-theme": "node scripts/preview-theme",
90
+ "close-stale-theme-prs": "node scripts/close-stale-theme-prs",
91
+ "generate-langs-json": "node scripts/generate-langs-json",
92
+ "format": "prettier --write .",
93
+ "format:check": "prettier --check .",
94
+ "lint": "NPM_CONFIG_LOGLEVEL=error npx eslint --max-warnings 0 \"./src/**/*.js\" \"./scripts/**/*.js\" \"./tests/**/*.js\" \"./api/**/*.js\" \"./themes/**/*.js\"",
95
+ "bench": "vitest run --config vitest.bench.config.js",
96
+ "deploy": "wrangler deploy",
97
+ "deploy:preview": "wrangler deploy --env preview",
98
+ "publish:npm": "pnpm publish --access public --no-git-checks --registry https://registry.npmjs.org",
99
+ "publish:github": "pnpm publish --access public --no-git-checks --registry https://npm.pkg.github.com",
100
+ "publish:all": "pnpm run publish:npm && pnpm run publish:github"
101
+ }
102
+ }