From 950d7e430b833f3e428f1af100f6fb74bdd76a2d Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Tue, 11 Aug 2026 17:41:01 -0400 Subject: Read the About box and latest release from JSON GitHub rebuilt the repository sidebar, so the selectors parseRepoInfo used matched nothing and the About box rendered empty. Everything but the latest release is in the page's embedded payload as sidebarAbout; the latest release is only in the _sidebar JSON already fetched for languages, so it arrives with that fragment and is moved beside the release count. --- src/scraper.ts | 126 ++++++++++++++++++++------------------------------------- 1 file changed, 43 insertions(+), 83 deletions(-) (limited to 'src/scraper.ts') diff --git a/src/scraper.ts b/src/scraper.ts index 223839f..4220dc4 100644 --- a/src/scraper.ts +++ b/src/scraper.ts @@ -143,7 +143,17 @@ interface CodeViewBlobRoute { richText: string | null; } +interface SidebarAbout { + description?: string | null; + website?: string | null; + stargazerCount?: number; + forksCount?: number; + // A section is false when the repository has none of that thing. + sections?: { releases?: { releaseCount?: number } | false }; +} + interface AppPayload { + sidebarAbout?: SidebarAbout; codeViewLayoutRoute?: CodeViewLayoutRoute; codeViewRepoRoute?: CodeViewRepoRoute; codeViewTreeRoute?: CodeViewTreeRoute; @@ -230,8 +240,6 @@ export interface RepoInfo { stars: string | null; forks: string | null; numReleases?: number; - latestReleaseTag?: string; - latestReleaseDate?: string; } export interface GitHubRepo extends GitHubTree { @@ -292,8 +300,14 @@ export interface Language { color: string | null; } -export interface GitHubLanguages extends GitHubCommon { +export interface LatestRelease { + tag: string; + publishedAt: string | null; +} + +export interface GitHubSidebar extends GitHubCommon { languages: Language[]; + latestRelease: LatestRelease | null; } export interface Ref { @@ -425,81 +439,6 @@ function parsePayload(html: string): AppPayload | null { return parseEmbeddedPayload(html); } -function parseRepoInfo(html: string): RepoInfo { - const document = parseDocument(html); - - const aboutSection = cssSelect.selectOne(".BorderGrid-cell h2:contains('About')", document); - let description: string | null = null; - let website: string | null = null; - - if (aboutSection) { - const parent = aboutSection.parent; - if (parent) { - const descElem = cssSelect.selectOne("p.f4", parent); - if (descElem) { - description = textContent(descElem).trim(); - } - const websiteElem = cssSelect.selectOne( - "a[rel='noopener noreferrer nofollow']", - parent, - ) as Element | null; - if (websiteElem) { - website = getAttributeValue(websiteElem, "href") || null; - } - } - } - - const starsElem = cssSelect.selectOne("#repo-stars-counter-star", document) as Element | null; - const stars = starsElem - ? getAttributeValue(starsElem, "title") || textContent(starsElem).trim() - : null; - - const forksElem = cssSelect.selectOne("#repo-network-counter", document) as Element | null; - const forks = forksElem - ? getAttributeValue(forksElem, "title") || textContent(forksElem).trim() - : null; - - const releaseElem = cssSelect.selectOne( - "a[href$='/releases'] .Counter", - document, - ) as Element | null; - let numReleases: number | undefined; - let latestReleaseTag: string | undefined; - let latestReleaseDate: string | undefined; - - if (releaseElem) { - const releaseText = getAttributeValue(releaseElem, "title") || textContent(releaseElem); - numReleases = parseInt(releaseText.replace(/,/g, ""), 10); - - // Find the latest release tag and date if available - const cell = (releaseElem.parent as Element)?.parent?.parent as Element; // span -> a -> h2 -> cell - if (cell) { - const latestLink = cssSelect.selectOne("a[href*='/releases/tag/']", cell) as Element; - if (latestLink) { - const tagElem = cssSelect.selectOne(".css-truncate-target", latestLink); - if (tagElem) { - latestReleaseTag = textContent(tagElem).trim(); - } - - const timeElem = cssSelect.selectOne("relative-time", latestLink); - if (timeElem) { - latestReleaseDate = getAttributeValue(timeElem, "datetime") || undefined; - } - } - } - } - - return { - description, - website, - stars, - forks, - numReleases, - latestReleaseTag, - latestReleaseDate, - }; -} - function extractOverviewHtml( overviewFiles: OverviewFile[] = [], ): Record | undefined { @@ -539,7 +478,15 @@ export async function getGitHubRepo(owner: string, repo: string): Promise { +// The repository sidebar (languages, releases, contributors, ...) is not +// rendered into the repo page at all; GitHub's own UI lazily fetches it as +// JSON from here. +export async function getGitHubSidebar(owner: string, repo: string): Promise { const payload = await fetchGitHubJson(`${owner}/${repo}/_sidebar`); const languages: Language[] = []; @@ -758,7 +710,15 @@ export async function getGitHubLanguages(owner: string, repo: string): Promise