|
| 1 | +// Refreshes stars/forks in showcase project frontmatter from the GitHub API. |
| 2 | +// Surgical line edits keep diffs minimal (no frontmatter reserialization). |
| 3 | +// Run via: node .github/scripts/update-showcase-stats.js |
| 4 | + |
| 5 | +/* eslint-disable @typescript-eslint/no-require-imports */ |
| 6 | +const fs = require('fs'); |
| 7 | +const path = require('path'); |
| 8 | + |
| 9 | +const SHOWCASE_DIR = path.join(process.cwd(), 'src/content/showcase'); |
| 10 | +const TOKEN = process.env.GITHUB_TOKEN; |
| 11 | + |
| 12 | +async function fetchRepo(repo) { |
| 13 | + const headers = { Accept: 'application/vnd.github+json', 'User-Agent': 'codestz-showcase-stats' }; |
| 14 | + if (TOKEN) headers.Authorization = `Bearer ${TOKEN}`; |
| 15 | + |
| 16 | + const res = await fetch(`https://api.github.com/repos/${repo}`, { headers }); |
| 17 | + if (!res.ok) { |
| 18 | + throw new Error(`GitHub API ${res.status} for ${repo}: ${await res.text()}`); |
| 19 | + } |
| 20 | + return res.json(); |
| 21 | +} |
| 22 | + |
| 23 | +// Split a file into its YAML frontmatter block and the rest. |
| 24 | +function splitFrontmatter(text) { |
| 25 | + const match = text.match(/^---\n([\s\S]*?)\n---/); |
| 26 | + if (!match) return null; |
| 27 | + return { block: match[1], start: match.index, end: match.index + match[0].length }; |
| 28 | +} |
| 29 | + |
| 30 | +function readField(block, key) { |
| 31 | + const m = block.match(new RegExp(`^${key}:\\s*['"]?([^'"\\n]+)['"]?\\s*$`, 'm')); |
| 32 | + return m ? m[1].trim() : null; |
| 33 | +} |
| 34 | + |
| 35 | +// Set `key: value` inside the frontmatter block. Replaces the line if present, |
| 36 | +// otherwise inserts it right after the `repo:` line. |
| 37 | +function setField(block, key, value) { |
| 38 | + const line = `${key}: ${value}`; |
| 39 | + const re = new RegExp(`^${key}:.*$`, 'm'); |
| 40 | + if (re.test(block)) return block.replace(re, line); |
| 41 | + return block.replace(/^(repo:.*)$/m, `$1\n${line}`); |
| 42 | +} |
| 43 | + |
| 44 | +async function main() { |
| 45 | + if (!fs.existsSync(SHOWCASE_DIR)) { |
| 46 | + console.log('No showcase directory, nothing to do.'); |
| 47 | + return; |
| 48 | + } |
| 49 | + |
| 50 | + const files = fs.readdirSync(SHOWCASE_DIR).filter((f) => f.endsWith('.mdx')); |
| 51 | + let changed = 0; |
| 52 | + |
| 53 | + for (const file of files) { |
| 54 | + const filePath = path.join(SHOWCASE_DIR, file); |
| 55 | + const text = fs.readFileSync(filePath, 'utf8'); |
| 56 | + const fm = splitFrontmatter(text); |
| 57 | + |
| 58 | + if (!fm) { |
| 59 | + console.warn(`! ${file}: no frontmatter, skipping`); |
| 60 | + continue; |
| 61 | + } |
| 62 | + |
| 63 | + const repo = readField(fm.block, 'repo'); |
| 64 | + if (!repo) { |
| 65 | + console.warn(`! ${file}: no repo field, skipping`); |
| 66 | + continue; |
| 67 | + } |
| 68 | + |
| 69 | + try { |
| 70 | + const data = await fetchRepo(repo); |
| 71 | + const stars = data.stargazers_count ?? 0; |
| 72 | + const forks = data.forks_count ?? 0; |
| 73 | + |
| 74 | + let block = setField(fm.block, 'stars', stars); |
| 75 | + block = setField(block, 'forks', forks); |
| 76 | + |
| 77 | + if (block !== fm.block) { |
| 78 | + const updated = `---\n${block}\n---` + text.slice(fm.end); |
| 79 | + fs.writeFileSync(filePath, updated); |
| 80 | + changed++; |
| 81 | + console.log(`✓ ${file}: ${repo} → ★${stars} ⑂${forks}`); |
| 82 | + } else { |
| 83 | + console.log(`= ${file}: ${repo} unchanged (★${stars} ⑂${forks})`); |
| 84 | + } |
| 85 | + } catch (err) { |
| 86 | + console.error(`! ${file}: ${err.message}`); |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + console.log(`Done. ${changed} file(s) updated.`); |
| 91 | +} |
| 92 | + |
| 93 | +main().catch((err) => { |
| 94 | + console.error(err); |
| 95 | + process.exit(1); |
| 96 | +}); |
0 commit comments