diff options
| author | Joe Mou <dev@mou.fo> | 2026-09-25 02:09:49 -0400 |
|---|---|---|
| committer | Joe Mou <dev@mou.fo> | 2026-09-25 02:15:25 -0400 |
| commit | 137831d6bcabcaa2851a09015b19d118cb054a38 (patch) | |
| tree | aecd2f17f69b1b0a014946b27e903da4b8fe921e /src/scraper.ts | |
| parent | 226984ad63b063c362e18260a591a3f9a035a8d3 (diff) | |
GitHub's pull request list no longer renders .js-issue-row rows server-side;
the rows come from the repoPullsDashboardContentRoute payload instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/scraper.ts')
| -rw-r--r-- | src/scraper.ts | 78 |
1 files changed, 23 insertions, 55 deletions
diff --git a/src/scraper.ts b/src/scraper.ts index ac1820a..7ec8611 100644 --- a/src/scraper.ts +++ b/src/scraper.ts @@ -211,6 +211,17 @@ interface CommitsRefRoute { commitGroups: CommitGroup[]; } +interface RepoPullsDashboardContentRoute { + results: { + number: number; + title: string; + createdAt: string; + // REPO_PULL_REQUEST_DISPLAY_STATE_OPEN, _CLOSED, _MERGED, or _DRAFT. + displayState: string; + labels?: IssueLabelNode[]; + }[]; +} + interface CodeViewBlobRoute { richText: string | null; renderedFileInfo?: { renderFileType: string | null } | null; @@ -234,6 +245,7 @@ interface AppPayload { "codeViewBlobLayoutRoute.StyledBlob"?: CodeViewBlobLayoutRouteStyledBlob; codeViewBlobRoute?: CodeViewBlobRoute; commitsRefRoute?: CommitsRefRoute; + repoPullsDashboardContentRoute?: RepoPullsDashboardContentRoute; // Actually there may be other query types. preloadedQueries?: PreloadedQuery[]; } @@ -788,31 +800,6 @@ function parseIssueLabel(label: IssueLabelNode): IssueLabel { return { name: label.name, color: label.color, description: label.description ?? null }; } -// A label in a list row wears its color as the RGB channels GitHub's own CSS -// mixes with, rather than as the hex its issue payloads carry, and keeps its -// description in a tooltip pointing back at it. -function parseRowLabel(label: Element, row: Element): IssueLabel { - const style = getAttributeValue(label, "style") ?? ""; - const channels: number[] = []; - for (const channel of ["r", "g", "b"]) { - const value = new RegExp(`--label-${channel}:\\s*(\\d+)`).exec(style); - if (value) channels.push(parseInt(value[1], 10)); - } - - const id = getAttributeValue(label, "id"); - const tooltip = id ? cssSelect.selectOne(`tool-tip[for="${id}"]`, row) : null; - - return { - name: getAttributeValue(label, "data-name") ?? textContent(label).trim(), - // GitHub's own default gray, for a label whose color it did not spell out. - color: - channels.length === 3 - ? channels.map((c) => c.toString(16).padStart(2, "0")).join("") - : "ededed", - description: tooltip ? textContent(tooltip).trim() || null : null, - }; -} - export async function getGitHubIssues( owner: string, repo: string, @@ -1032,38 +1019,19 @@ export async function getGitHubPulls( ): Promise<GitHubPulls> { const suffix = q ? `?q=${encodeURIComponent(q)}` : ""; const html = await fetchGitHubPage(`${owner}/${repo}/pulls${suffix}`); - const document = parseDocument(html); - - const pulls: Issue[] = []; - for (const row of cssSelect.selectAll(".js-issue-row", document) as unknown as Element[]) { - const id = getAttributeValue(row, "id"); // "issue_NNN" - const number = id ? parseInt(id.replace("issue_", ""), 10) : 0; - if (!number) continue; - - const titleLink = cssSelect.selectOne("a.markdown-title", row); - const title = titleLink ? textContent(titleLink).trim() : ""; - - const timeElem = cssSelect.selectOne("relative-time", row); - const createdAt = timeElem ? getAttributeValue(timeElem, "datetime") || "" : ""; - - const stateSpan = cssSelect.selectOne(".tooltipped[aria-label]", row) as Element | null; - const ariaLabel = stateSpan ? (getAttributeValue(stateSpan, "aria-label") ?? "") : ""; - const state = ariaLabel.includes("Merged") - ? "MERGED" - : ariaLabel.includes("Closed") - ? "CLOSED" - : ariaLabel.includes("Draft") - ? "DRAFT" - : "OPEN"; - - const labels: IssueLabel[] = []; - for (const label of cssSelect.selectAll("a.IssueLabel", row) as unknown as Element[]) { - labels.push(parseRowLabel(label, row)); - } - - pulls.push({ number, title, state, createdAt, labels }); + const route = parsePayload(html)?.repoPullsDashboardContentRoute; + if (route === undefined) { + throw new Error("Could not find repoPullsDashboardContentRoute in embedded JSON"); } + const pulls: Issue[] = route.results.map((result) => ({ + number: result.number, + title: result.title, + state: result.displayState.replace(/^REPO_PULL_REQUEST_DISPLAY_STATE_/, ""), + createdAt: result.createdAt, + labels: (result.labels ?? []).map(parseIssueLabel), + })); + return { repo: { owner, name: repo }, tabs: parseRepoTabs(html), pulls, q }; } |
