when-am-i / files / scripts / gen-badges.mts
gen-badges.mts
| 1 | // 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 |
| 9 | // files) come from disk; the static badges (demo, tech stack) are |
| 10 | // emitted here too so the whole row has a single source of truth. |
| 11 | |
| 12 | import { readFile, writeFile, readdir } from 'node:fs/promises'; |
| 13 | import path from 'node:path'; |
| 14 | import { fileURLToPath } from 'node:url'; |
| 15 | |
| 16 | const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); |
| 17 | const README = path.join(REPO_ROOT, 'README.md'); |
| 18 | |
| 19 | const PAGES_URL = 'https://apps.fuzzyworld.net/when-am-i/'; |
| 20 | const START = '<!-- badges:start -->'; |
| 21 | const END = '<!-- badges:end -->'; |
| 22 | const 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. */ |
| 25 | const 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). */ |
| 29 | async 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 | |
| 34 | async 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 | `[})](${PAGES_URL})`, |
| 41 | `})`, |
| 42 | `, 'orange')})`, |
| 43 | `})`, |
| 44 | '', |
| 45 | '', |
| 46 | '', |
| 47 | '', |
| 48 | '', |
| 49 | ].join('\n'); |
| 50 | } |
| 51 | |
| 52 | async 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 | |
| 79 | main().catch((err) => { |
| 80 | console.error(err); |
| 81 | process.exit(1); |
| 82 | }); |