Files
ComfyUI_frontend/apps/website/src/utils/github.ts
guill e02ee17d3d fix(website): memoize GitHub stars fetch to one call per build (#12495)
*PR Created by the Glary-Bot Agent*

---

## Problem

The GitHub star badge silently disappears from the comfy.org navigation.
Verified by curl-ing the live homepage:

```
props="{..."github-stars":[0,""]}"
```

`SiteNav.vue` only renders the badge when `githubStars` is truthy, so an
empty string hides it.

## Root cause

`apps/website/src/layouts/BaseLayout.astro` `await`s
`fetchGitHubStars('Comfy-Org', 'ComfyUI')` in its frontmatter. Astro
evaluates layout frontmatter **per rendered page** in SSG. With 379
pages (46 source `.astro` files × locales/dynamic routes), the
unauthenticated GitHub REST endpoint is called hundreds of times per
build, blasting past the 60 req/h anonymous rate limit. Once GitHub
returns 403 the existing `try/catch` returns `null`, `githubStars`
becomes `''`, and the badge vanishes — with no log line to indicate why.

## Fix

Cache the in-flight promise in a module-scope `Map` keyed by
`${owner}/${repo}` so every page in a single build shares one request.
Already-resolved counts stay cached, and the existing
`WEBSITE_GITHUB_STARS_OVERRIDE` env-var escape hatch still
short-circuits first.

While in the file:
- Pass an injectable `fetchImpl` so tests can stub without
`vi.spyOn(globalThis, 'fetch')`.
- Replace the implicit-`any` `data.stargazers_count ?? null` with a
narrow `readStargazerCount(data: unknown)` guard.
- In `BaseLayout.astro`, change `rawStars ? ...` to `rawStars !== null ?
...` so a hypothetical 0-star repo wouldn't be hidden (the old check
treated 0 as missing).

## Verification

- `pnpm --filter @comfyorg/website test:unit` → 89/89 pass (5 new test
cases: memoization, per-key isolation, non-2xx → null, throw → null,
override).
- `pnpm typecheck:website` → 0 errors.
- `pnpm format:check` → clean.
- `pnpm --filter @comfyorg/website build` → 379 pages built; with no
override set, output HTML contains `"github-stars":[0,"115K"]` (the live
count) on every page; with `WEBSITE_GITHUB_STARS_OVERRIDE=110000`, it
contains `"110K"` and `fetch` is never called.
- Playwright on the local preview confirms the badge renders at the
top-right of the nav with `aria-label="ComfyUI on GitHub — 110K stars"`.

## Scope

102 lines changed across 3 files (40 non-test). Deliberately leaves the
broader "snapshot fallback / build-data source" refactor to the existing
`codex/website-github-stars-once` branch — this PR just unblocks the
user-visible symptom.

## Screenshots

![Site navigation showing the 110K GitHub star badge restored next to
the DOWNLOAD LOCAL / LAUNCH CLOUD
buttons](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/d921afa7b2cb2a9088080967634aeb3e2e67ee09a8ac13f6e434c1c7589434c1/pr-images/1779918781152-ccaeab7f-9150-4d8b-ae4e-f20bbf49091b.png)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-05-29 03:04:41 +00:00

74 lines
1.9 KiB
TypeScript

const inflight = new Map<string, Promise<number | null>>()
export function resetGitHubStarsFetcherForTests(): void {
inflight.clear()
}
export async function fetchGitHubStars(
owner: string,
repo: string,
fetchImpl: typeof fetch = fetch
): Promise<number | null> {
const override = readGitHubStarsOverride()
if (override !== undefined) return override
const key = `${owner}/${repo}`
const cached = inflight.get(key)
if (cached) return cached
const request = doFetch(owner, repo, fetchImpl)
inflight.set(key, request)
return request
}
async function doFetch(
owner: string,
repo: string,
fetchImpl: typeof fetch
): Promise<number | null> {
try {
const res = await fetchImpl(
`https://api.github.com/repos/${owner}/${repo}`,
{ headers: { Accept: 'application/vnd.github.v3+json' } }
)
if (!res.ok) return null
const data: unknown = await res.json()
return readStargazerCount(data)
} catch {
return null
}
}
function readStargazerCount(data: unknown): number | null {
if (data === null || typeof data !== 'object') return null
if (!('stargazers_count' in data)) return null
const count = data.stargazers_count
return typeof count === 'number' ? count : null
}
export function formatStarCount(count: number): string {
if (count >= 1_000_000) {
const m = count / 1_000_000
return `${m >= 10 ? Math.round(m) : m.toFixed(1).replace(/\.0$/, '')}M`
}
if (count >= 1_000) {
const k = count / 1_000
return `${k >= 10 ? Math.round(k) : k.toFixed(1).replace(/\.0$/, '')}K`
}
return count.toString()
}
function readGitHubStarsOverride(): number | undefined {
const rawCount = process.env.WEBSITE_GITHUB_STARS_OVERRIDE
if (rawCount === undefined || rawCount === '') return undefined
const count = Number(rawCount)
if (!Number.isSafeInteger(count) || count < 0) {
throw new Error(
'WEBSITE_GITHUB_STARS_OVERRIDE must be a non-negative integer'
)
}
return count
}