Skip to content

Commit 23af6e1

Browse files
Make API header state reliable
1 parent 7b8fe32 commit 23af6e1

6 files changed

Lines changed: 71 additions & 50 deletions

File tree

.github/workflows/website.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ jobs:
3030
- run: pnpm install --frozen-lockfile
3131
- id: pages
3232
uses: actions/configure-pages@v5
33+
- name: Read repository stars
34+
env:
35+
GH_TOKEN: ${{ github.token }}
36+
run: |
37+
stars="$(gh api "repos/$GITHUB_REPOSITORY" --jq .stargazers_count)"
38+
echo "API_REFERENCE_GITHUB_STARS=$stars" >> "$GITHUB_ENV"
3339
- run: pnpm docs:site
3440
env:
3541
API_REFERENCE_BASE_PATH: ${{ steps.pages.outputs.base_path }}

CONTRIBUTING.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,7 @@ The release workflow calls the GitHub Pages workflow after Changesets publishes
7777
a package. The Pages workflow can also be run manually to deploy the current
7878
commit before a release without invoking the package-release job. Pages supplies
7979
`API_REFERENCE_BASE_PATH` during the build so project URLs and custom domains
80-
use the same generated site without configuration edits.
80+
use the same generated site without configuration edits. It also reads the
81+
repository star count through GitHub's API and supplies it as
82+
`API_REFERENCE_GITHUB_STARS`, keeping the deployed badge independent of
83+
browser-side API access.

scripts/api-reference-site/api-reference-site.test.mjs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
githubRepository,
66
moduleRoute,
77
normalizeBasePath,
8+
normalizeGitHubStars,
89
normalizeOrigin,
910
renderIndexPage,
1011
renderLayout,
@@ -66,6 +67,7 @@ test("normalizes root, project, and custom-domain base paths", () => {
6667
const site = {
6768
basePath: "/docs/",
6869
description: "Schema-first state machines",
70+
githubStars: 1_234,
6971
modules: [{
7072
api: { declarationCount: 12, description: "State machine APIs" },
7173
export: "./Machine",
@@ -109,12 +111,36 @@ test("links the header to the repository root and exposes its star-count target"
109111
})
110112
assert.match(
111113
html,
112-
/href="https:\/\/github\.com\/typeonce-dev\/effect-machine" aria-label="View typeonce-dev\/effect-machine on GitHub"/
114+
/href="https:\/\/github\.com\/typeonce-dev\/effect-machine" aria-label="View typeonce-dev\/effect-machine on GitHub \(1,234 GitHub stars\)"/
113115
)
114-
assert.match(html, /data-github-stars="typeonce-dev\/effect-machine" hidden/)
116+
assert.match(html, /class="github-stars" title="1,234 GitHub stars"/)
117+
assert.match(html, /<span>1,234<\/span>/)
118+
assert.doesNotMatch(html, /class="github-stars"[^>]*(?:data-github-stars|hidden)/)
115119
assert.doesNotMatch(html, /github-link[^>]+\/tree\//)
116120
})
117121

122+
test("omits the star badge when the build does not supply a count", () => {
123+
const html = renderLayout({ ...site, githubStars: undefined }, {
124+
content: "",
125+
currentRoute: "",
126+
pageKind: "overview",
127+
title: "Effect Machine"
128+
})
129+
assert.match(html, /aria-label="View typeonce-dev\/effect-machine on GitHub"/)
130+
assert.doesNotMatch(html, /class="github-stars"/)
131+
})
132+
133+
test("accepts only non-negative safe integers for build-time GitHub stars", () => {
134+
assert.equal(normalizeGitHubStars(undefined), undefined)
135+
assert.equal(normalizeGitHubStars(""), undefined)
136+
assert.equal(normalizeGitHubStars("0"), 0)
137+
assert.equal(normalizeGitHubStars("1234"), 1_234)
138+
assert.throws(() => normalizeGitHubStars("-1"), /non-negative integer/)
139+
assert.throws(() => normalizeGitHubStars("1.5"), /non-negative integer/)
140+
assert.throws(() => normalizeGitHubStars("01"), /non-negative integer/)
141+
assert.throws(() => normalizeGitHubStars("9007199254740992"), /safe integer range/)
142+
})
143+
118144
test("accepts only root GitHub repository URLs for the header integration", () => {
119145
assert.equal(githubRepository("https://github.com/typeonce-dev/effect-machine"), "typeonce-dev/effect-machine")
120146
assert.throws(

scripts/api-reference-site/assets/client.js

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ const searchDialog = document.querySelector("[data-search-dialog]")
88
const searchInput = document.querySelector("[data-search-input]")
99
const searchStatus = document.querySelector("[data-search-status]")
1010
const searchResults = document.querySelector("[data-search-results]")
11-
const githubStars = document.querySelector("[data-github-stars]")
1211

1312
const themes = ["auto", "light", "dark"]
14-
const themeLabels = { auto: "System theme", light: "Light theme", dark: "Dark theme" }
13+
const themeLabels = { auto: "System", light: "Light", dark: "Dark" }
1514

1615
const updateThemeButton = () => {
1716
const theme = root.dataset.theme ?? "auto"
18-
themeButton.textContent = theme === "dark" ? "Light" : theme === "light" ? "Dark" : "Theme"
19-
themeButton.title = themeLabels[theme]
17+
const label = themeLabels[theme]
18+
themeButton.textContent = label
19+
themeButton.title = `Current theme: ${label}`
2020
}
2121

2222
themeButton?.addEventListener("click", () => {
@@ -28,39 +28,6 @@ themeButton?.addEventListener("click", () => {
2828
})
2929
updateThemeButton()
3030

31-
const showGitHubStars = (count) => {
32-
const countElement = githubStars?.querySelector("[data-github-star-count]")
33-
if (githubStars === null || countElement === null || !Number.isSafeInteger(count) || count < 0) return
34-
countElement.textContent = new Intl.NumberFormat(undefined, {
35-
maximumFractionDigits: 1,
36-
notation: count >= 1_000 ? "compact" : "standard"
37-
}).format(count)
38-
githubStars.title = `${count.toLocaleString()} GitHub star${count === 1 ? "" : "s"}`
39-
githubStars.hidden = false
40-
}
41-
42-
const loadGitHubStars = async () => {
43-
const repository = githubStars?.dataset.githubStars
44-
if (repository === undefined) return
45-
const cacheKey = `api-reference:github-stars:${repository}`
46-
try {
47-
const cached = sessionStorage.getItem(cacheKey)
48-
if (cached !== null) {
49-
showGitHubStars(Number(cached))
50-
return
51-
}
52-
const response = await fetch(`https://api.github.com/repos/${repository}`)
53-
if (!response.ok) return
54-
const body = await response.json()
55-
if (!Number.isSafeInteger(body.stargazers_count) || body.stargazers_count < 0) return
56-
sessionStorage.setItem(cacheKey, String(body.stargazers_count))
57-
showGitHubStars(body.stargazers_count)
58-
} catch {
59-
// The repository link remains usable when storage or GitHub is unavailable.
60-
}
61-
}
62-
void loadGitHubStars()
63-
6431
const setNavigationOpen = (open) => {
6532
document.body.classList.toggle("navigation-is-open", open)
6633
navigationButton?.setAttribute("aria-expanded", String(open))

scripts/api-reference-site/assets/styles.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,6 @@ kbd {
241241
font-variant-numeric: tabular-nums;
242242
}
243243

244-
.github-stars[hidden] {
245-
display: none;
246-
}
247-
248244
.github-stars svg {
249245
fill: currentColor;
250246
}

scripts/api-reference-site/generate.mjs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,10 @@ export const renderLayout = (site, { content, currentRoute, description, pageKin
296296

297297
const renderHeader = (site) => {
298298
const repository = githubRepository(site.package.repositoryUrl)
299+
const stars = renderGitHubStars(site.githubStars)
300+
const githubLabel = stars === ""
301+
? `View ${repository} on GitHub`
302+
: `View ${repository} on GitHub (${githubStarsLabel(site.githubStars)})`
299303
return `
300304
<a class="skip-link" href="#main-content">Skip to content</a>
301305
<header class="site-header" data-pagefind-ignore>
@@ -312,18 +316,24 @@ const renderHeader = (site) => {
312316
<span>Search the API</span>
313317
<kbd>⌘ K</kbd>
314318
</button>
315-
<a class="header-link github-link" href="${escapeAttribute(site.package.repositoryUrl)}" aria-label="View ${escapeAttribute(repository)} on GitHub">
319+
<a class="header-link github-link" href="${escapeAttribute(site.package.repositoryUrl)}" aria-label="${escapeAttribute(githubLabel)}">
316320
<span class="github-link__label">GitHub</span>
317-
<span class="github-stars" data-github-stars="${escapeAttribute(repository)}" hidden>
318-
<svg aria-hidden="true" viewBox="0 0 16 16" width="14" height="14"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.193a.75.75 0 0 1-1.088.79L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194-3.047-2.97a.75.75 0 0 1 .416-1.278l4.21-.612L7.327.668A.75.75 0 0 1 8 .25Z"/></svg>
319-
<span data-github-star-count></span>
320-
</span>
321+
${stars}
321322
</a>
322323
<button class="icon-button theme-button" type="button" aria-label="Change color theme" title="Change color theme">Theme</button>
323324
</div>
324325
</header>`
325326
}
326327

328+
const renderGitHubStars = (count) => count === undefined ? "" : `
329+
<span class="github-stars" title="${escapeAttribute(githubStarsLabel(count))}">
330+
<svg aria-hidden="true" viewBox="0 0 16 16" width="14" height="14"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.193a.75.75 0 0 1-1.088.79L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194-3.047-2.97a.75.75 0 0 1 .416-1.278l4.21-.612L7.327.668A.75.75 0 0 1 8 .25Z"/></svg>
331+
<span>${new Intl.NumberFormat("en-US").format(count)}</span>
332+
</span>`
333+
334+
const githubStarsLabel = (count) =>
335+
`${new Intl.NumberFormat("en-US").format(count)} GitHub star${count === 1 ? "" : "s"}`
336+
327337
export const githubRepository = (value) => {
328338
const url = new URL(value)
329339
const segments = url.pathname.split("/").filter(Boolean)
@@ -542,8 +552,21 @@ const readConfig = (path) => {
542552
return {
543553
...config,
544554
origin: normalizeOrigin(config.origin),
545-
basePath: normalizeBasePath(process.env.API_REFERENCE_BASE_PATH ?? config.basePath)
555+
basePath: normalizeBasePath(process.env.API_REFERENCE_BASE_PATH ?? config.basePath),
556+
githubStars: normalizeGitHubStars(process.env.API_REFERENCE_GITHUB_STARS)
557+
}
558+
}
559+
560+
export const normalizeGitHubStars = (value) => {
561+
if (value === undefined || value === "") return undefined
562+
if (!/^(0|[1-9]\d*)$/.test(value)) {
563+
throw new Error("API_REFERENCE_GITHUB_STARS must be a non-negative integer")
564+
}
565+
const count = Number(value)
566+
if (!Number.isSafeInteger(count)) {
567+
throw new Error("API_REFERENCE_GITHUB_STARS exceeds the safe integer range")
546568
}
569+
return count
547570
}
548571

549572
export const normalizeOrigin = (value) => {

0 commit comments

Comments
 (0)