fuzzyworld

when-am-i / files / scripts / gen-badges.mts

gen-badges.mts

Blame: the commit that last changed each line · file · history

6bcd5898 2026-06-19 Pavel Safronov1// Generate the README badge block from real project data, so the badges never
2// drift out of sync with the repo. Run before opening a PR:
3//
4// npm run gen:badges # rewrite the badge block in README.md
5// npm run gen:badges -- --check # exit non-zero if it's stale (CI / pre-PR gate)
6//
7// The block lives between the <!-- badges:start --> and <!-- badges:end -->
8// markers in README.md. Computed values (app version, number of calendar data
c2d75cfd 2026-10-27 Pavel Safronov9// files) come from disk; the static badges (demo, tech stack) are
6bcd5898 2026-06-19 Pavel Safronov10// emitted here too so the whole row has a single source of truth.
11
12import { readFile, writeFile, readdir } from 'node:fs/promises';
13import path from 'node:path';
14import { fileURLToPath } from 'node:url';
15
16const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17const README = path.join(REPO_ROOT, 'README.md');
18
c2d75cfd 2026-10-27 Pavel Safronov19const PAGES_URL = 'https://apps.fuzzyworld.net/when-am-i/';
6bcd5898 2026-06-19 Pavel Safronov20const START = '<!-- badges:start -->';
21const END = '<!-- badges:end -->';
22const PREAMBLE = '<!-- generated by `npm run gen:badges` — do not edit by hand -->';
23
24/** A shields.io static badge URL: label-message-color, each part encoded. */
25const shield = (label: string, message: string, color: string) =>
26 `https://img.shields.io/badge/${encodeURIComponent(label)}-${encodeURIComponent(message)}-${color}`;
27
28/** Number of calendar data files (everything under calendars/ except the schema). */
29async function countCalendars(): Promise<number> {
30 const files = await readdir(path.join(REPO_ROOT, 'src/data/calendars'));
31 return files.filter((f) => f.endsWith('.json') && !f.startsWith('SCHEMA')).length;
32}
33
34async function buildBlock(): Promise<string> {
35 const pkg = JSON.parse(await readFile(path.join(REPO_ROOT, 'package.json'), 'utf-8')) as { version: string };
36 const calendars = await countCalendars();
37
38 return [
39 PREAMBLE,
40 `[![live demo](${shield('demo', 'live', 'success')})](${PAGES_URL})`,
41 `![version](${shield('version', `v${pkg.version}`, 'blue')})`,
42 `![calendar data](${shield('calendar data', String(calendars), 'orange')})`,
43 `![license](${shield('license', 'MIT', 'green')})`,
44 '',
45 '![React](https://img.shields.io/badge/React-20232A?logo=react&logoColor=61DAFB)',
46 '![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=fff)',
47 '![Vite](https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=fff)',
48 '![Leaflet](https://img.shields.io/badge/Leaflet-199900?logo=leaflet&logoColor=fff)',
49 ].join('\n');
50}
51
52async function main() {
53 const check = process.argv.includes('--check');
54 const readme = await readFile(README, 'utf-8');
55
56 const startIdx = readme.indexOf(START);
57 const endIdx = readme.indexOf(END);
58 if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
59 throw new Error(`README.md is missing the ${START} … ${END} markers`);
60 }
61
62 const next =
63 readme.slice(0, startIdx + START.length) +
64 '\n' + await buildBlock() + '\n' +
65 readme.slice(endIdx);
66
67 if (next === readme) {
68 console.log('✅ README badges already up to date');
69 return;
70 }
71 if (check) {
72 console.error('❌ README badges are stale — run `npm run gen:badges` and commit the result');
73 process.exit(1);
74 }
75 await writeFile(README, next);
76 console.log('✨ Updated README badges');
77}
78
79main().catch((err) => {
80 console.error(err);
81 process.exit(1);
82});