@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.
- package/LICENSE +21 -0
- package/api/gist.js +98 -0
- package/api/index.js +146 -0
- package/api/pin.js +114 -0
- package/api/status/pat-info.js +193 -0
- package/api/status/up.js +129 -0
- package/api/top-langs.js +163 -0
- package/api/wakatime.js +129 -0
- package/package.json +102 -0
- package/readme.md +412 -0
- package/src/calculateRank.js +87 -0
- package/src/cards/gist.js +151 -0
- package/src/cards/index.js +4 -0
- package/src/cards/repo.js +199 -0
- package/src/cards/stats.js +607 -0
- package/src/cards/top-languages.js +968 -0
- package/src/cards/types.d.ts +67 -0
- package/src/cards/wakatime.js +482 -0
- package/src/common/Card.js +294 -0
- package/src/common/I18n.js +41 -0
- package/src/common/access.js +69 -0
- package/src/common/api-utils.js +221 -0
- package/src/common/blacklist.js +10 -0
- package/src/common/cache.js +153 -0
- package/src/common/color.js +194 -0
- package/src/common/envs.js +15 -0
- package/src/common/error.js +84 -0
- package/src/common/fmt.js +90 -0
- package/src/common/html.js +43 -0
- package/src/common/http.js +24 -0
- package/src/common/icons.js +63 -0
- package/src/common/index.js +13 -0
- package/src/common/languageColors.json +651 -0
- package/src/common/log.js +14 -0
- package/src/common/ops.js +124 -0
- package/src/common/render.js +261 -0
- package/src/common/retryer.js +97 -0
- package/src/common/worker-adapter.js +148 -0
- package/src/common/worker-env.js +48 -0
- package/src/fetchers/gist.js +114 -0
- package/src/fetchers/repo.js +118 -0
- package/src/fetchers/stats.js +350 -0
- package/src/fetchers/top-languages.js +192 -0
- package/src/fetchers/types.d.ts +118 -0
- package/src/fetchers/wakatime.js +109 -0
- package/src/index.js +2 -0
- package/src/translations.js +1105 -0
- package/src/worker.ts +116 -0
- package/themes/README.md +229 -0
- package/themes/index.js +467 -0
package/api/top-langs.js
ADDED
|
@@ -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
|
+
};
|
package/api/wakatime.js
ADDED
|
@@ -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
|
+
}
|