From 1ce8d783d6ea57121a0b5c0639a98f1474f11187 Mon Sep 17 00:00:00 2001 From: shawn Date: Wed, 1 Jul 2026 23:03:05 +0800 Subject: [PATCH 1/2] chore: remove release download counts --- README.md | 4 + README_CN.md | 4 + src/_locales/en/messages.json | 8 - src/_locales/zh_CN/messages.json | 8 - src/_locales/zh_TW/messages.json | 8 - src/content.ts | 10 - src/features/release-asset-downloads.test.ts | 209 ------------------- src/features/release-asset-downloads.ts | 196 ----------------- src/lib/github-api.ts | 20 -- src/lib/messages.ts | 10 - src/options.test.ts | 1 - src/options.ts | 1 - src/service-worker.test.ts | 47 ----- src/service-worker.ts | 52 ----- src/styles/content.css | 93 --------- static/options.html | 12 -- 16 files changed, 8 insertions(+), 675 deletions(-) delete mode 100644 src/features/release-asset-downloads.test.ts delete mode 100644 src/features/release-asset-downloads.ts diff --git a/README.md b/README.md index 3fcc83a..bd6512d 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,10 @@ Every feature can be individually toggled on or off in the settings. All features except Recent Commit Message Color can be individually toggled on/off in the extension options. +## Removed Features + +- **Release Asset Downloads** — Removed after GitHub updated the Releases page to show release asset download counts natively. Discovered on 2026-07-01. + ## Build from Source 1. Clone the repo and build: diff --git a/README_CN.md b/README_CN.md index b24bb81..4a74a87 100644 --- a/README_CN.md +++ b/README_CN.md @@ -135,6 +135,10 @@ Better GitHub 是一个增强 GitHub 使用体验的 Chrome 插件,灵感源 除"最近提交 Commit Message 颜色"外,所有功能均可在扩展选项中单独开关。 +## 已删除功能 + +- **Release 资源下载量** — 已删除。2026 年 7 月 1 日发现 GitHub 官方更新了 Release 页面,已原生支持显示 release asset 下载量。 + ## 自行打包安装 1. 克隆仓库并构建: diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index f1eaf3d..93dfde4 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -107,10 +107,6 @@ "featReleaseTabDesc": { "message": "Add a Releases tab to the repository navigation bar for quick access." }, - "featReleaseDownloadsName": { "message": "Release Asset Downloads" }, - "featReleaseDownloadsDesc": { - "message": "Show how many times each release asset has been downloaded on the Releases page." - }, "featWatchForkStarName": { "message": "Watch/Fork/Star Popup" }, "featWatchForkStarDesc": { "message": "Hover over Watch, Fork, or Star counts to preview the list in a popup." @@ -146,10 +142,6 @@ "message": "Tag: $name$", "placeholders": { "name": { "content": "$1", "example": "v1.0.0" } } }, - "assetDownloadsTitle": { - "message": "Downloaded $count$ times", - "placeholders": { "count": { "content": "$1", "example": "1,234" } } - }, "copied": { "message": "Copied!" }, "branchCopyTitle": { "message": "Click to copy branch name" }, diff --git a/src/_locales/zh_CN/messages.json b/src/_locales/zh_CN/messages.json index c1c0c4e..e27fec3 100644 --- a/src/_locales/zh_CN/messages.json +++ b/src/_locales/zh_CN/messages.json @@ -107,10 +107,6 @@ "featReleaseTabDesc": { "message": "在仓库导航栏添加一个 Releases 标签页,方便快速访问。" }, - "featReleaseDownloadsName": { "message": "Release 资源下载量" }, - "featReleaseDownloadsDesc": { - "message": "在 Releases 页面显示每个资源文件被下载的次数。" - }, "featWatchForkStarName": { "message": "Watch/Fork/Star 弹窗" }, "featWatchForkStarDesc": { "message": "悬停在 Watch、Fork 或 Star 数字上,即可在弹窗中预览列表。" @@ -146,10 +142,6 @@ "message": "Tag: $name$", "placeholders": { "name": { "content": "$1", "example": "v1.0.0" } } }, - "assetDownloadsTitle": { - "message": "已下载 $count$ 次", - "placeholders": { "count": { "content": "$1", "example": "1,234" } } - }, "copied": { "message": "已复制!" }, "branchCopyTitle": { "message": "点击复制分支名" }, diff --git a/src/_locales/zh_TW/messages.json b/src/_locales/zh_TW/messages.json index b9bbd3f..2b96822 100644 --- a/src/_locales/zh_TW/messages.json +++ b/src/_locales/zh_TW/messages.json @@ -107,10 +107,6 @@ "featReleaseTabDesc": { "message": "在儲存庫導覽列新增一個 Releases 分頁,方便快速存取。" }, - "featReleaseDownloadsName": { "message": "Release 資源下載次數" }, - "featReleaseDownloadsDesc": { - "message": "在 Releases 頁面顯示每個資源檔案被下載的次數。" - }, "featWatchForkStarName": { "message": "Watch/Fork/Star 彈出視窗" }, "featWatchForkStarDesc": { "message": "將游標停在 Watch、Fork 或 Star 數字上,即可在彈出視窗中預覽清單。" @@ -146,10 +142,6 @@ "message": "Tag: $name$", "placeholders": { "name": { "content": "$1", "example": "v1.0.0" } } }, - "assetDownloadsTitle": { - "message": "已下載 $count$ 次", - "placeholders": { "count": { "content": "$1", "example": "1,234" } } - }, "copied": { "message": "已複製!" }, "branchCopyTitle": { "message": "點擊複製分支名稱" }, diff --git a/src/content.ts b/src/content.ts index edae685..7b52a86 100644 --- a/src/content.ts +++ b/src/content.ts @@ -4,10 +4,6 @@ import { injectPRBranchNames } from "./features/pr-branch-names"; import { injectPRReviewStatus } from "./features/pr-review-status"; import { injectPRDiffStats } from "./features/pr-diff-stats"; import { injectReleasesTab } from "./features/release-tab"; -import { - injectReleaseAssetDownloads, - cleanupReleaseAssetDownloads, -} from "./features/release-asset-downloads"; import { injectPRLabelPosition, cleanupPRLabelPosition } from "./features/pr-label-position"; import { injectFileAgeColor } from "./features/file-age-color"; import { injectPRApproveNow } from "./features/pr-approve-now"; @@ -29,7 +25,6 @@ const FEATURE_KEYS = [ "feature-pr-review-status", "feature-pr-diff-stats", "feature-release-tab", - "feature-release-downloads-count", "feature-pr-label-position", "feature-pr-approve-now", "feature-default-sort", @@ -49,7 +44,6 @@ const FEATURE_CLASSES: Record = { "feature-pr-review-status": ["better-github-review-status"], "feature-pr-diff-stats": ["better-github-diff-stats", "bg-skeleton-pill--pr-diff"], "feature-release-tab": ["better-github-releases-tab"], - "feature-release-downloads-count": ["better-github-asset-downloads"], "feature-pr-label-position": ["better-github-label-prefix"], "feature-pr-approve-now": ["better-github-approve-now", "better-github-approve-dialog-overlay"], "feature-default-sort": [], @@ -67,7 +61,6 @@ const FEATURE_CLASSES: Record = { const FEATURE_CLEANUPS: Partial void>> = { "feature-pr-label-position": cleanupPRLabelPosition, "feature-watch-fork-star-popup": cleanupWatchForkStarPopup, - "feature-release-downloads-count": cleanupReleaseAssetDownloads, "feature-contributor-card": cleanupContributorCard, }; @@ -117,9 +110,6 @@ function injectFeature(key: FeatureKey): void { case "feature-release-tab": injectReleasesTab(); break; - case "feature-release-downloads-count": - injectReleaseAssetDownloads(); - break; case "feature-pr-label-position": injectPRLabelPosition(); break; diff --git a/src/features/release-asset-downloads.test.ts b/src/features/release-asset-downloads.test.ts deleted file mode 100644 index 933b585..0000000 --- a/src/features/release-asset-downloads.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { setUrl } from "../test-utils/url"; -import { - injectReleaseAssetDownloads, - cleanupReleaseAssetDownloads, - parseAssetHref, - getDownloadHeat, -} from "./release-asset-downloads"; -import { fetchReleaseDownloads } from "../lib/github-api"; - -// Run the feature on canned data instead of the content<->worker bridge. -vi.mock("../lib/github-api"); - -const GH = "https://github.com"; -const BADGE = ".better-github-asset-downloads"; - -// A release asset row mirroring GitHub's Box-row layout: a download link on the -// left, and a metadata column on the right holding size + date spans. -function assetRow(tag: string, name: string, size = "1.9 KB"): string { - return ` -
  • - -
    - ${size} - x -
    -
  • - `; -} - -describe("parseAssetHref", () => { - it("extracts tag and name from a download URL", () => { - expect(parseAssetHref("/owner/repo/releases/download/v1.0.0/app.zip", "owner", "repo")).toEqual( - { - tag: "v1.0.0", - name: "app.zip", - }, - ); - }); - - it("decodes encoded names and slashed tags", () => { - expect( - parseAssetHref("/owner/repo/releases/download/release%2F1.0/my%20app.zip", "owner", "repo"), - ).toEqual({ tag: "release/1.0", name: "my app.zip" }); - }); - - it("ignores query strings and falls back to the marker when the repo prefix differs", () => { - expect(parseAssetHref("/o/r/releases/download/v2/file.tar.gz?x=1", "owner", "repo")).toEqual({ - tag: "v2", - name: "file.tar.gz", - }); - }); - - it("returns null for non-asset hrefs", () => { - expect(parseAssetHref(null, "owner", "repo")).toBeNull(); - expect(parseAssetHref("/owner/repo/archive/refs/tags/v1.zip", "owner", "repo")).toBeNull(); - }); -}); - -describe("getDownloadHeat", () => { - it("maps download volume onto a 1..10 heat scale", () => { - expect(getDownloadHeat(0)).toBe(1); - expect(getDownloadHeat(99)).toBe(1); - expect(getDownloadHeat(100)).toBe(2); - expect(getDownloadHeat(999)).toBe(3); - expect(getDownloadHeat(1000)).toBe(4); - expect(getDownloadHeat(1_000_000)).toBe(10); - expect(getDownloadHeat(50_000_000)).toBe(10); - }); - - it("is monotonic in the download count", () => { - let prev = 0; - for (const count of [0, 50, 200, 800, 2000, 8000, 20000, 80000, 200000, 800000, 5_000_000]) { - const heat = getDownloadHeat(count); - expect(heat).toBeGreaterThanOrEqual(prev); - prev = heat; - } - }); -}); - -describe("injectReleaseAssetDownloads", () => { - beforeEach(() => { - vi.clearAllMocks(); - setUrl(`${GH}/owner/repo/releases`); - document.body.innerHTML = ""; - }); - - afterEach(() => { - // Disconnect the MutationObserver and clear claim markers between tests. - cleanupReleaseAssetDownloads(); - vi.restoreAllMocks(); - }); - - it("appends the count to the asset-name column, right-aligned, with formatted count", async () => { - document.body.innerHTML = `
      ${assetRow("v1.0.0", "app.zip")}
    `; - vi.mocked(fetchReleaseDownloads).mockResolvedValue([ - { tag: "v1.0.0", name: "app.zip", downloadCount: 1234 }, - ]); - - await injectReleaseAssetDownloads(); - - const badge = document.querySelector(BADGE) as HTMLAnchorElement; - expect(badge).not.toBeNull(); - expect(badge.tagName).toBe("A"); - expect(badge.textContent).toBe((1234).toLocaleString()); - expect(badge.title).toBe(`Downloaded ${(1234).toLocaleString()} times`); - // It links to the asset's own download URL, so the icon is clickable. - expect(badge.getAttribute("href")).toBe("/owner/repo/releases/download/v1.0.0/app.zip"); - // Lives in the name column (the column holding the download link), pushed to - // its end — decoupled from the digest / size / date columns. - const nameLink = badge.parentElement?.querySelector(".Truncate") as HTMLElement; - expect(nameLink).not.toBeNull(); - expect(badge.parentElement?.lastElementChild).toBe(badge); - // Carries the heat level so CSS can tint it by download volume. - expect(badge.dataset.betterGithubDlHeat).toBe(String(getDownloadHeat(1234))); - }); - - it("leaves assets without a matching count untouched", async () => { - document.body.innerHTML = `
      ${assetRow("v1.0.0", "app.zip")}${assetRow("v1.0.0", "other.zip")}
    `; - vi.mocked(fetchReleaseDownloads).mockResolvedValue([ - { tag: "v1.0.0", name: "app.zip", downloadCount: 5 }, - ]); - - await injectReleaseAssetDownloads(); - - expect(document.querySelectorAll(BADGE)).toHaveLength(1); - }); - - it("groups assets by release tag and fetches each release once", async () => { - document.body.innerHTML = `
      ${assetRow("v2.0.0", "a.zip")}${assetRow("v2.0.0", "b.zip")}${assetRow("v1.0.0", "c.zip")}
    `; - vi.mocked(fetchReleaseDownloads).mockImplementation((_o, _r, tag) => - Promise.resolve( - tag === "v2.0.0" - ? [ - { tag, name: "a.zip", downloadCount: 11 }, - { tag, name: "b.zip", downloadCount: 22 }, - ] - : [{ tag, name: "c.zip", downloadCount: 33 }], - ), - ); - - await injectReleaseAssetDownloads(); - - // One fetch per distinct tag (not per asset), with the tag parsed from the link. - expect(fetchReleaseDownloads).toHaveBeenCalledTimes(2); - expect(fetchReleaseDownloads).toHaveBeenCalledWith("owner", "repo", "v2.0.0"); - expect(fetchReleaseDownloads).toHaveBeenCalledWith("owner", "repo", "v1.0.0"); - expect(Array.from(document.querySelectorAll(BADGE), (b) => b.textContent)).toEqual([ - "11", - "22", - "33", - ]); - }); - - it("badges only assets whose name the fetched release returns", async () => { - document.body.innerHTML = `
      ${assetRow("v1.0.0", "present.zip")}${assetRow("v1.0.0", "missing.zip")}
    `; - vi.mocked(fetchReleaseDownloads).mockResolvedValue([ - { tag: "v1.0.0", name: "present.zip", downloadCount: 9 }, - ]); - - await injectReleaseAssetDownloads(); - - expect(document.querySelectorAll(BADGE)).toHaveLength(1); - expect(document.querySelector(BADGE)?.textContent).toBe("9"); - }); - - it("is idempotent across repeated runs", async () => { - document.body.innerHTML = `
      ${assetRow("v1.0.0", "app.zip")}
    `; - vi.mocked(fetchReleaseDownloads).mockResolvedValue([ - { tag: "v1.0.0", name: "app.zip", downloadCount: 7 }, - ]); - - await injectReleaseAssetDownloads(); - await injectReleaseAssetDownloads(); - - expect(document.querySelectorAll(BADGE)).toHaveLength(1); - }); - - it("badges assets that GitHub renders later, via the MutationObserver", async () => { - document.body.innerHTML = `
      `; - vi.mocked(fetchReleaseDownloads).mockResolvedValue([ - { tag: "v3.0.0", name: "late.zip", downloadCount: 42 }, - ]); - - await injectReleaseAssetDownloads(); - expect(document.querySelector(BADGE)).toBeNull(); - expect(fetchReleaseDownloads).not.toHaveBeenCalled(); - - // Simulate GitHub lazy-loading a release's assets (Show all / expand). - document.getElementById("list")!.innerHTML = assetRow("v3.0.0", "late.zip"); - - await vi.waitFor(() => expect(document.querySelector(BADGE)).not.toBeNull()); - expect(document.querySelector(BADGE)?.textContent).toBe("42"); - expect(fetchReleaseDownloads).toHaveBeenCalledWith("owner", "repo", "v3.0.0"); - }); - - it("does nothing when there are no asset links", async () => { - document.body.innerHTML = `
      • no assets
      `; - - await injectReleaseAssetDownloads(); - - expect(fetchReleaseDownloads).not.toHaveBeenCalled(); - expect(document.querySelector(BADGE)).toBeNull(); - }); -}); diff --git a/src/features/release-asset-downloads.ts b/src/features/release-asset-downloads.ts deleted file mode 100644 index cd80383..0000000 --- a/src/features/release-asset-downloads.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { isReleasesPage, getRepoInfo, type RepoInfo } from "../lib/page-detect"; -import { fetchReleaseDownloads } from "../lib/github-api"; -import { escapeHtml } from "../lib/utils"; -import { t } from "../lib/i18n"; - -const BADGE_CLASS = "better-github-asset-downloads"; - -// Octicon download-16 — matches GitHub's own icon set. -const DOWNLOAD_ICON = ``; - -interface ParsedAsset { - tag: string; - name: string; -} - -/** - * Parse a release-asset download URL into its tag + file name. - * URLs look like `/{owner}/{repo}/releases/download/{tag}/{name}`; both the tag - * and name are URL-encoded, and tags may themselves contain encoded slashes. - */ -export function parseAssetHref( - href: string | null, - owner: string, - repo: string, -): ParsedAsset | null { - if (!href) return null; - const path = href.split(/[?#]/)[0]; - - const marker = "/releases/download/"; - const ownerPrefix = `/${owner}/${repo}${marker}`; - let rest: string; - if (path.startsWith(ownerPrefix)) { - rest = path.slice(ownerPrefix.length); - } else { - const idx = path.indexOf(marker); - if (idx === -1) return null; - rest = path.slice(idx + marker.length); - } - - const segments = rest.split("/"); - if (segments.length < 2) return null; - const nameSeg = segments.pop(); - if (!nameSeg) return null; - - const decode = (s: string): string => { - try { - return decodeURIComponent(s); - } catch { - return s; - } - }; - - return { tag: decode(segments.join("/")), name: decode(nameSeg) }; -} - -// The badge lives at the right edge of the asset-name column (pushed there with -// margin-left:auto in CSS), deliberately decoupled from the digest / size / date -// columns so the variable-width count never shifts their alignment. Return the -// top-level row child that contains the download link. -function findNameColumn(link: Element, row: Element): HTMLElement | null { - for (const child of Array.from(row.children)) { - if (child.contains(link)) return child as HTMLElement; - } - return null; -} - -// Download counts span orders of magnitude, so bucket them on a roughly log -// scale: every threshold crossed bumps the heat one level. 1 = quietest (few -// downloads, stays muted), 10 = hottest (most-downloaded, full accent color). -const DOWNLOAD_HEAT_THRESHOLDS = [100, 300, 1000, 3000, 10000, 30000, 100000, 300000, 1000000]; - -export function getDownloadHeat(count: number): number { - let level = 1; - for (const threshold of DOWNLOAD_HEAT_THRESHOLDS) { - if (count >= threshold) level++; - } - return level; -} - -// The badge is a link to the asset's own download URL, so the download icon is -// real: clicking it downloads the file, same as clicking the asset name. -function buildBadge(count: number, href: string): HTMLAnchorElement { - const formatted = count.toLocaleString(); - const badge = document.createElement("a"); - badge.className = BADGE_CLASS; - badge.href = href; - badge.rel = "nofollow"; - // Let the browser download the file instead of letting Turbo try to render it. - badge.setAttribute("data-turbo", "false"); - badge.dataset.betterGithubDlHeat = String(getDownloadHeat(count)); - badge.title = t("assetDownloadsTitle", formatted); - badge.innerHTML = `${escapeHtml(formatted)}${DOWNLOAD_ICON}`; - return badge; -} - -interface PendingAsset { - link: HTMLAnchorElement; - name: string; - href: string; -} - -let observer: MutationObserver | null = null; -let rafId: number | null = null; - -// Fetch counts for a single release tag, then badge each of its visible assets. -// One fetch per tag (the service worker caches + coalesces), so the latest -// release costs ~45 KB instead of pulling the whole release history. -async function injectTag(info: RepoInfo, tag: string, items: PendingAsset[]): Promise { - const downloads = await fetchReleaseDownloads(info.owner, info.repo, tag); - if (downloads.length === 0) return; - - const counts = new Map(); - for (const d of downloads) counts.set(d.name, d.downloadCount); - - for (const { link, name, href } of items) { - const count = counts.get(name); - if (count === undefined) continue; - - const row = link.closest("li"); - if (!row || row.querySelector(`.${BADGE_CLASS}`)) continue; - - const nameCol = findNameColumn(link, row); - if (!nameCol) continue; - - nameCol.appendChild(buildBadge(count, href)); - } -} - -// Badge every asset link not yet handled, grouped by release tag. Each link is -// claimed (data attribute) synchronously, before any await, so the observer -// firing mid-fetch never double-processes a row. Resolves once this pass's tag -// fetches have settled, so callers/tests can await it. -async function processVisibleAssets(info: RepoInfo): Promise { - const links = document.querySelectorAll( - `a[href*="/releases/download/"]:not(.${BADGE_CLASS}):not([data-bg-dl-seen])`, - ); - if (links.length === 0) return; - - const byTag = new Map(); - for (const link of links) { - link.dataset.bgDlSeen = "1"; - const href = link.getAttribute("href"); - const parsed = parseAssetHref(href, info.owner, info.repo); - if (!href || !parsed) continue; - const item: PendingAsset = { link, name: parsed.name, href }; - const group = byTag.get(parsed.tag); - if (group) group.push(item); - else byTag.set(parsed.tag, [item]); - } - - await Promise.all([...byTag].map(([tag, items]) => injectTag(info, tag, items))); -} - -export async function injectReleaseAssetDownloads(): Promise { - observer?.disconnect(); - observer = null; - if (rafId !== null) { - cancelAnimationFrame(rafId); - rafId = null; - } - - if (!isReleasesPage()) return; - - const info = getRepoInfo(); - if (!info) return; - - // GitHub renders the latest release's assets eagerly but paginates them - // ("Show all N assets"), and lazy-loads every other release's assets via - // on expand. Watch for those late-arriving rows, then - // process whatever is already on the page. - // Coalesce mutation bursts (lazy-loads fire many in a row) into a single - // full-page scan per frame, instead of re-scanning on every batch. - observer = new MutationObserver(() => { - if (rafId !== null) return; - rafId = requestAnimationFrame(() => { - rafId = null; - void processVisibleAssets(info); - }); - }); - observer.observe(document.body, { childList: true, subtree: true }); - - await processVisibleAssets(info); -} - -export function cleanupReleaseAssetDownloads(): void { - observer?.disconnect(); - observer = null; - if (rafId !== null) { - cancelAnimationFrame(rafId); - rafId = null; - } - // Drop claim markers so re-enabling the feature re-badges the same rows. - document.querySelectorAll("[data-bg-dl-seen]").forEach((el) => { - delete el.dataset.bgDlSeen; - }); -} diff --git a/src/lib/github-api.ts b/src/lib/github-api.ts index a4a6668..80b6691 100644 --- a/src/lib/github-api.ts +++ b/src/lib/github-api.ts @@ -10,7 +10,6 @@ import type { StargazerInfo, WatcherInfo, ForkInfo, - ReleaseAssetDownload, ContributorInfo, } from "./messages"; @@ -49,7 +48,6 @@ export type { StargazerInfo, WatcherInfo, ForkInfo, - ReleaseAssetDownload, ContributorInfo, }; @@ -237,21 +235,3 @@ export async function fetchForks(owner: string, repo: string): Promise { - try { - return await sendMessage({ - type: "FETCH_RELEASE_DOWNLOADS", - owner, - repo, - tag, - }); - } catch (err) { - console.error("[Better GitHub] Failed to fetch release downloads:", err); - return []; - } -} diff --git a/src/lib/messages.ts b/src/lib/messages.ts index f4189b7..d0a483f 100644 --- a/src/lib/messages.ts +++ b/src/lib/messages.ts @@ -69,15 +69,6 @@ export interface ForkInfo { stargazersCount: number; } -export interface ReleaseAssetDownload { - /** The release tag the asset belongs to (raw `tag_name`, matches the download URL). */ - tag: string; - /** Asset file name (matches the last segment of the download URL). */ - name: string; - /** Number of times this asset file has been downloaded. */ - downloadCount: number; -} - /** Objective facts about a GitHub account, for the contributor background card. * Pure-fact only — no scoring. See docs/pr-signals-plan.md. */ export interface ContributorInfo { @@ -115,7 +106,6 @@ export type ServiceWorkerRequest = | { type: "FETCH_STARGAZERS"; owner: string; repo: string } | { type: "FETCH_WATCHERS"; owner: string; repo: string } | { type: "FETCH_FORKS"; owner: string; repo: string } - | { type: "FETCH_RELEASE_DOWNLOADS"; owner: string; repo: string; tag: string } | { type: "FETCH_CONTRIBUTOR_INFO"; login: string; owner?: string; repo?: string }; export type ServiceWorkerResponse = { ok: true; data: T } | { ok: false; error: string }; diff --git a/src/options.test.ts b/src/options.test.ts index d72966b..02fb123 100644 --- a/src/options.test.ts +++ b/src/options.test.ts @@ -5,7 +5,6 @@ const FEATURE_KEYS = [ "feature-pr-review-status", "feature-pr-diff-stats", "feature-release-tab", - "feature-release-downloads-count", "feature-pr-label-position", "feature-pr-approve-now", "feature-default-sort", diff --git a/src/options.ts b/src/options.ts index abdf438..d4563db 100644 --- a/src/options.ts +++ b/src/options.ts @@ -33,7 +33,6 @@ const FEATURE_KEYS = [ "feature-pr-review-status", "feature-pr-diff-stats", "feature-release-tab", - "feature-release-downloads-count", "feature-pr-label-position", "feature-pr-approve-now", "feature-default-sort", diff --git a/src/service-worker.test.ts b/src/service-worker.test.ts index ad97898..072d438 100644 --- a/src/service-worker.test.ts +++ b/src/service-worker.test.ts @@ -493,51 +493,4 @@ describe("service worker", () => { expect(vi.mocked(fetch)).toHaveBeenCalledTimes(1); }); - it("fetches a single release by tag and maps its assets", async () => { - const state = await loadWorker("token"); - vi.mocked(fetch).mockResolvedValue( - jsonResponse({ - tag_name: "v2.0.0", - assets: [ - { name: "app-linux.tar.gz", download_count: 1500 }, - { name: "app-macos.zip", download_count: 42 }, - ], - }), - ); - - const response = await sendMessage(state.messageListeners[0], { - type: "FETCH_RELEASE_DOWNLOADS", - owner: "owner", - repo: "repo", - tag: "v2.0.0", - }); - - expect(response).toEqual({ - ok: true, - data: [ - { tag: "v2.0.0", name: "app-linux.tar.gz", downloadCount: 1500 }, - { tag: "v2.0.0", name: "app-macos.zip", downloadCount: 42 }, - ], - }); - expect(vi.mocked(fetch).mock.calls[0][0]).toBe( - "https://api.github.com/repos/owner/repo/releases/tags/v2.0.0", - ); - }); - - it("encodes the tag and works anonymously, tolerating a release with no assets", async () => { - const state = await loadWorker(); - vi.mocked(fetch).mockResolvedValue(jsonResponse({ tag_name: "release/1.0" })); - - const response = await sendMessage(state.messageListeners[0], { - type: "FETCH_RELEASE_DOWNLOADS", - owner: "owner", - repo: "repo", - tag: "release/1.0", - }); - - expect(response).toEqual({ ok: true, data: [] }); - expect(vi.mocked(fetch).mock.calls[0][0]).toBe( - "https://api.github.com/repos/owner/repo/releases/tags/release%2F1.0", - ); - }); }); diff --git a/src/service-worker.ts b/src/service-worker.ts index 8cbd697..f6dce4d 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -11,7 +11,6 @@ import type { StargazerInfo, WatcherInfo, ForkInfo, - ReleaseAssetDownload, ContributorInfo, } from "./lib/messages"; @@ -565,52 +564,6 @@ async function fetchForks(owner: string, repo: string): Promise { }); } -interface RawReleaseAsset { - name: string; - download_count: number; -} - -interface RawRelease { - tag_name: string; - assets?: RawReleaseAsset[]; -} - -function mapReleaseAssets(release: RawRelease): ReleaseAssetDownload[] { - if (!release?.tag_name || !Array.isArray(release.assets)) return []; - return release.assets.map((asset) => ({ - tag: release.tag_name, - name: asset.name, - downloadCount: asset.download_count, - })); -} - -// Per-asset download counts for a single release, fetched by tag. The content -// script asks only for tags whose assets are actually visible (the latest -// release, plus any the user expands), so we never over-fetch the whole release -// history. Works anonymously on public repos; a token only raises the rate limit. -async function fetchReleaseDownloads( - owner: string, - repo: string, - tag: string, -): Promise { - const cacheKey = `cache:releasedl:${owner}/${repo}:tag:${tag}`; - return cachedFetch(cacheKey, async () => { - const headers = restHeaders(await getToken(), "application/vnd.github+json"); - const url = `https://api.github.com/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(tag)}`; - const response = await fetch(url, { headers }); - - if (!response.ok) { - console.error( - `[Better GitHub] Release downloads API error: ${response.status} ${response.statusText}`, - ); - return []; - } - - const release: RawRelease = await response.json(); - return mapReleaseAssets(release); - }); -} - // --- Contributor background card --- // // Objective facts about an account, fetched lazily on hover (one user at a @@ -788,11 +741,6 @@ async function handleMessage( return { ok: true, data: await fetchWatchers(request.owner, request.repo) }; case "FETCH_FORKS": return { ok: true, data: await fetchForks(request.owner, request.repo) }; - case "FETCH_RELEASE_DOWNLOADS": - return { - ok: true, - data: await fetchReleaseDownloads(request.owner, request.repo, request.tag), - }; case "FETCH_CONTRIBUTOR_INFO": return { ok: true, diff --git a/src/styles/content.css b/src/styles/content.css index c96be4e..cd63b1d 100644 --- a/src/styles/content.css +++ b/src/styles/content.css @@ -246,99 +246,6 @@ a.better-github-review-popover-item:hover { color: var(--fgColor-muted, var(--color-fg-muted, #656d76)); } -/* Release Asset Downloads — count pinned to the right edge of the asset-name - column, right-aligned, so it never shifts the digest / size / date columns. */ -.better-github-asset-downloads { - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: 4px; - margin-left: auto; - padding-left: 16px; - padding-right: 16px; - flex-shrink: 0; - white-space: nowrap; - font-size: 12px; - font-weight: 500; - line-height: 18px; - text-align: right; - text-decoration: none; - color: var(--fgColor-muted, var(--color-fg-muted, #656d76)) !important; -} - -/* It's a link to the asset's download URL — surface that on hover. */ -.better-github-asset-downloads:hover { - text-decoration: underline; -} - -.better-github-asset-downloads svg { - flex-shrink: 0; -} - -/* Download heat — tint the count by volume: quiet (muted) for few downloads, - intensifying toward the same heat color as the file-age map for the most- - downloaded assets. Same color-mix blend, keyed to magnitude instead of - recency. Level 1 (fewest) stays the default muted, so it needs no rule. */ -.better-github-asset-downloads[data-better-github-dl-heat="2"] { - color: color-mix( - in srgb, - var(--better-github-heat-color) 12%, - var(--fgColor-muted, var(--color-fg-muted, #656d76)) - ) !important; -} -.better-github-asset-downloads[data-better-github-dl-heat="3"] { - color: color-mix( - in srgb, - var(--better-github-heat-color) 22%, - var(--fgColor-muted, var(--color-fg-muted, #656d76)) - ) !important; -} -.better-github-asset-downloads[data-better-github-dl-heat="4"] { - color: color-mix( - in srgb, - var(--better-github-heat-color) 33%, - var(--fgColor-muted, var(--color-fg-muted, #656d76)) - ) !important; -} -.better-github-asset-downloads[data-better-github-dl-heat="5"] { - color: color-mix( - in srgb, - var(--better-github-heat-color) 44%, - var(--fgColor-muted, var(--color-fg-muted, #656d76)) - ) !important; -} -.better-github-asset-downloads[data-better-github-dl-heat="6"] { - color: color-mix( - in srgb, - var(--better-github-heat-color) 56%, - var(--fgColor-muted, var(--color-fg-muted, #656d76)) - ) !important; -} -.better-github-asset-downloads[data-better-github-dl-heat="7"] { - color: color-mix( - in srgb, - var(--better-github-heat-color) 67%, - var(--fgColor-muted, var(--color-fg-muted, #656d76)) - ) !important; -} -.better-github-asset-downloads[data-better-github-dl-heat="8"] { - color: color-mix( - in srgb, - var(--better-github-heat-color) 78%, - var(--fgColor-muted, var(--color-fg-muted, #656d76)) - ) !important; -} -.better-github-asset-downloads[data-better-github-dl-heat="9"] { - color: color-mix( - in srgb, - var(--better-github-heat-color) 89%, - var(--fgColor-muted, var(--color-fg-muted, #656d76)) - ) !important; -} -.better-github-asset-downloads[data-better-github-dl-heat="10"] { - color: var(--better-github-heat-color) !important; -} - /* File Age Color — heat map for recent commits */ :root { --better-github-heat-color: #c24e00; diff --git a/static/options.html b/static/options.html index 700a454..025ff3f 100644 --- a/static/options.html +++ b/static/options.html @@ -731,18 +731,6 @@

      Settings

      -
    • -
      -
      Release Asset Downloads
      -
      - Show how many times each release asset has been downloaded. -
      -
      - -
    • Watch/Fork/Star Popup
      From 4c0ad8e4a35493a68197673e540c4370c0273366 Mon Sep 17 00:00:00 2001 From: shawn Date: Wed, 1 Jul 2026 23:27:12 +0800 Subject: [PATCH 2/2] update AGENTS.md --- AGENTS.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2333748..73c1332 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ pnpm test:coverage # run with v8 coverage report (coverage/index.html) regressions**, meant to be ratcheted up — not a target to pad. They only apply under `--coverage`, so plain `pnpm test` (CI) is unaffected. - Prefer tests that assert observable behavior over ones that merely execute - code for coverage. Coverage proves code *ran*, not that it was *verified*. + code for coverage. Coverage proves code _ran_, not that it was _verified_. ## Performance @@ -48,9 +48,6 @@ it competes with GitHub's rendering for the same main thread and network. - **Fetch lazily, scoped to what's visible.** Never eagerly pull a whole dataset when the user can only see a fraction of it. Fetch per visible item, and only when it's actually on screen (or on expand) — not the entire history up front. - Example: release download counts are fetched per visible release tag, not by - pulling the full release history. (This was originally written the eager way - and had to be fixed — see commit `perf: fetch release downloads lazily`.) - **Debounce and scope DOM observers.** A `MutationObserver` on `document.body` fires constantly. Coalesce bursts into one pass per frame with `requestAnimationFrame` (see `pr-collapse-expand.ts`), or scope the work to