Skip to content

Commit 3285878

Browse files
committed
web: fetch community stats at build time
1 parent 9014a15 commit 3285878

6 files changed

Lines changed: 196 additions & 4 deletions

File tree

.web/docs/.vitepress/config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,26 @@ const ogUrl = 'https://gate.minekube.com';
1212
const ogImage = `${ogUrl}/og-image.png`;
1313
const ogTitle = 'Gate Proxy';
1414
const ogDescription = 'Next Generation Minecraft Proxy';
15+
const numberFromEnv = (value: string | undefined, fallback: number) => {
16+
const parsed = Number(value);
17+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
18+
};
19+
const communityStats = {
20+
discordMembers: numberFromEnv(process.env.GATE_DOCS_DISCORD_MEMBERS, 1650),
21+
githubStars: numberFromEnv(process.env.GATE_DOCS_GITHUB_STARS, 1050),
22+
};
1523

1624
export default defineConfig({
1725
title: `Gate Proxy${additionalTitle}`,
1826
description: ogDescription,
1927
appearance: 'dark',
2028

29+
vite: {
30+
define: {
31+
__COMMUNITY_STATS__: communityStats,
32+
},
33+
},
34+
2135
sitemap: {
2236
hostname: ogUrl,
2337
},

.web/docs/shared/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,22 @@ export const discordLink = 'https://minekube.com/discord';
44
export const gitHubLink = 'https://github.com/minekube';
55

66
// Community stats
7+
declare const __COMMUNITY_STATS__:
8+
| {
9+
discordMembers: number;
10+
githubStars: number;
11+
}
12+
| undefined;
13+
714
export const communityStats = {
8-
discordMembers: 1350,
9-
githubStars: 900,
15+
discordMembers:
16+
typeof __COMMUNITY_STATS__ !== 'undefined'
17+
? __COMMUNITY_STATS__.discordMembers
18+
: 1650,
19+
githubStars:
20+
typeof __COMMUNITY_STATS__ !== 'undefined'
21+
? __COMMUNITY_STATS__.githubStars
22+
: 1050,
1023
};
1124

1225
export const editLink = (project: string): DefaultTheme.EditLink => {

.web/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
"author": "Minekube",
66
"scripts": {
77
"dev": "pnpm exec vitepress dev docs",
8-
"build": "pnpm exec vitepress build docs",
9-
"serve": "pnpm exec vitepress serve docs"
8+
"build": "node scripts/build-with-community-stats.mjs",
9+
"serve": "pnpm exec vitepress serve docs",
10+
"test": "node --test scripts/*.test.mjs"
1011
},
1112
"devDependencies": {
1213
"@cloudflare/workers-types": "^4.20251117.0",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { spawn } from 'node:child_process';
2+
3+
import { buildStatsEnv, fetchCommunityStats } from './community-stats.mjs';
4+
5+
const stats = await fetchCommunityStats();
6+
const statsEnv = buildStatsEnv(stats);
7+
8+
console.log(
9+
`[community-stats] Discord members: ${statsEnv.GATE_DOCS_DISCORD_MEMBERS}; GitHub stars: ${statsEnv.GATE_DOCS_GITHUB_STARS}`
10+
);
11+
12+
const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
13+
const child = spawn(command, ['exec', 'vitepress', 'build', 'docs'], {
14+
env: {
15+
...process.env,
16+
...statsEnv,
17+
},
18+
stdio: 'inherit',
19+
});
20+
21+
child.on('exit', (code, signal) => {
22+
if (signal) {
23+
process.kill(process.pid, signal);
24+
return;
25+
}
26+
process.exit(code ?? 1);
27+
});

.web/scripts/community-stats.mjs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
export const DEFAULT_COMMUNITY_STATS = {
2+
discordMembers: 1650,
3+
githubStars: 1050,
4+
};
5+
6+
export const DEFAULT_DISCORD_INVITE_CODE = 'HvQugYx';
7+
export const DEFAULT_GITHUB_REPO = 'minekube/gate';
8+
9+
const isValidCount = (value) => Number.isInteger(value) && value >= 0;
10+
11+
export function parseGitHubRepoStats(payload) {
12+
const count = payload?.stargazers_count;
13+
if (!isValidCount(count)) {
14+
throw new Error('GitHub API response did not include stargazers_count');
15+
}
16+
return count;
17+
}
18+
19+
export function parseDiscordInviteStats(payload) {
20+
const count = payload?.approximate_member_count;
21+
if (!isValidCount(count)) {
22+
throw new Error(
23+
'Discord invite API response did not include approximate_member_count'
24+
);
25+
}
26+
return count;
27+
}
28+
29+
async function fetchJson(url, { fetchImpl = fetch, headers = {} } = {}) {
30+
const response = await fetchImpl(url, {
31+
headers: {
32+
Accept: 'application/json',
33+
...headers,
34+
},
35+
signal: AbortSignal.timeout(10_000),
36+
});
37+
38+
if (!response.ok) {
39+
throw new Error(`Request failed with ${response.status} for ${url}`);
40+
}
41+
42+
return response.json();
43+
}
44+
45+
export async function fetchGitHubStars({
46+
fetchImpl,
47+
repo = process.env.GATE_GITHUB_REPO || DEFAULT_GITHUB_REPO,
48+
} = {}) {
49+
const payload = await fetchJson(`https://api.github.com/repos/${repo}`, {
50+
fetchImpl,
51+
headers: {
52+
'X-GitHub-Api-Version': '2022-11-28',
53+
'User-Agent': 'gate-docs-build',
54+
},
55+
});
56+
return parseGitHubRepoStats(payload);
57+
}
58+
59+
export async function fetchDiscordMembers({
60+
fetchImpl,
61+
inviteCode = process.env.GATE_DISCORD_INVITE_CODE ||
62+
DEFAULT_DISCORD_INVITE_CODE,
63+
} = {}) {
64+
const payload = await fetchJson(
65+
`https://discord.com/api/v10/invites/${inviteCode}?with_counts=true`,
66+
{ fetchImpl }
67+
);
68+
return parseDiscordInviteStats(payload);
69+
}
70+
71+
export async function fetchCommunityStats({ fetchImpl, logger = console } = {}) {
72+
const stats = {};
73+
74+
try {
75+
stats.discordMembers = await fetchDiscordMembers({ fetchImpl });
76+
} catch (error) {
77+
logger.warn(
78+
`[community-stats] Discord member count unavailable, using fallback: ${error.message}`
79+
);
80+
}
81+
82+
try {
83+
stats.githubStars = await fetchGitHubStars({ fetchImpl });
84+
} catch (error) {
85+
logger.warn(
86+
`[community-stats] GitHub star count unavailable, using fallback: ${error.message}`
87+
);
88+
}
89+
90+
return stats;
91+
}
92+
93+
export function buildStatsEnv(stats = {}) {
94+
const discordMembers = isValidCount(stats.discordMembers)
95+
? stats.discordMembers
96+
: DEFAULT_COMMUNITY_STATS.discordMembers;
97+
const githubStars = isValidCount(stats.githubStars)
98+
? stats.githubStars
99+
: DEFAULT_COMMUNITY_STATS.githubStars;
100+
101+
return {
102+
GATE_DOCS_DISCORD_MEMBERS: String(discordMembers),
103+
GATE_DOCS_GITHUB_STARS: String(githubStars),
104+
};
105+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import assert from 'node:assert/strict';
2+
import test from 'node:test';
3+
4+
import {
5+
DEFAULT_COMMUNITY_STATS,
6+
buildStatsEnv,
7+
parseDiscordInviteStats,
8+
parseGitHubRepoStats,
9+
} from './community-stats.mjs';
10+
11+
test('parseGitHubRepoStats reads stargazers_count', () => {
12+
assert.equal(parseGitHubRepoStats({ stargazers_count: 1050 }), 1050);
13+
});
14+
15+
test('parseDiscordInviteStats reads approximate_member_count', () => {
16+
assert.equal(parseDiscordInviteStats({ approximate_member_count: 1652 }), 1652);
17+
});
18+
19+
test('stats parsers reject missing or invalid values', () => {
20+
assert.throws(() => parseGitHubRepoStats({ stargazers_count: -1 }));
21+
assert.throws(() => parseDiscordInviteStats({ approximate_member_count: '1652' }));
22+
});
23+
24+
test('buildStatsEnv preserves fetched counts and falls back per field', () => {
25+
assert.deepEqual(
26+
buildStatsEnv({ discordMembers: 1652, githubStars: undefined }),
27+
{
28+
GATE_DOCS_DISCORD_MEMBERS: '1652',
29+
GATE_DOCS_GITHUB_STARS: String(DEFAULT_COMMUNITY_STATS.githubStars),
30+
}
31+
);
32+
});

0 commit comments

Comments
 (0)