Skip to content

Commit 914e5f3

Browse files
feat: leaderboard in README + dedicated /leaderboard page (#176)
* docs(readme): add leaderboard section with top 10 from benchmarks repo Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * ci(leaderboard): refresh README leaderboard from benchmarks json on schedule Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * feat(website): add /leaderboard page driven by react-doctor-benchmarks Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * chore(scripts): drop bench:scores in favor of leaderboard.json Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * docs(readme): trim leaderboard table to repo + score Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * docs(readme): drop raw-results link from leaderboard section Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * fix(scripts): make update-leaderboard idempotent against formatter Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * refactor(website): extract score thresholds, color, label, doctor face to shared utils Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
1 parent 01c38a7 commit 914e5f3

13 files changed

Lines changed: 387 additions & 233 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Update Leaderboard
2+
3+
on:
4+
schedule:
5+
- cron: "17 7 * * *"
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
12+
jobs:
13+
update:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v5
17+
with:
18+
token: ${{ secrets.GITHUB_TOKEN }}
19+
20+
- uses: pnpm/action-setup@v4
21+
22+
- uses: actions/setup-node@v4
23+
with:
24+
node-version: 22
25+
cache: pnpm
26+
27+
- run: pnpm install --frozen-lockfile
28+
29+
- name: Update leaderboard section in README
30+
run: pnpm leaderboard:update
31+
32+
- name: Format
33+
run: pnpm format
34+
35+
- name: Commit and push if changed
36+
run: |
37+
if [ -z "$(git status --porcelain)" ]; then
38+
echo "No leaderboard changes."
39+
exit 0
40+
fi
41+
git config user.name "github-actions[bot]"
42+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
43+
git add packages/react-doctor/README.md
44+
git commit -m "chore(readme): refresh leaderboard top 10"
45+
git push

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"changeset": "changeset",
2424
"version": "changeset version",
2525
"release": "pnpm build && changeset publish",
26-
"bench:scores": "bun scripts/benchmark-scores.ts"
26+
"leaderboard:update": "node --experimental-strip-types --no-warnings scripts/update-leaderboard.ts"
2727
},
2828
"devDependencies": {
2929
"@changesets/cli": "^2.31.0",

packages/react-doctor/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,29 @@ const counts = summarizeDiagnostics(result.diagnostics);
197197

198198
`react-doctor/api` re-exports `JsonReport`, `JsonReportSummary`, `JsonReportProjectEntry`, `JsonReportMode`, plus the lower-level `buildJsonReport` and `buildJsonReportError` builders. See [`packages/react-doctor/src/api.ts`](https://github.com/millionco/react-doctor/blob/main/packages/react-doctor/src/api.ts) for the full types.
199199

200+
## Leaderboard
201+
202+
Top React codebases scanned by React Doctor, ranked by score. Updated automatically from [millionco/react-doctor-benchmarks](https://github.com/millionco/react-doctor-benchmarks).
203+
204+
<!-- LEADERBOARD:START -->
205+
<!-- prettier-ignore -->
206+
| # | Repo | Score |
207+
| -- | ---- | ----: |
208+
| 1 | [executor](https://github.com/RhysSullivan/executor) | 96 |
209+
| 2 | [nodejs.org](https://github.com/nodejs/nodejs.org) | 87 |
210+
| 3 | [tldraw](https://github.com/tldraw/tldraw) | 76 |
211+
| 4 | [t3code](https://github.com/pingdotgg/t3code) | 75 |
212+
| 5 | [mastra](https://github.com/mastra-ai/mastra) | 70 |
213+
| 6 | [excalidraw](https://github.com/excalidraw/excalidraw) | 69 |
214+
| 7 | [payload](https://github.com/payloadcms/payload) | 69 |
215+
| 8 | [better-auth](https://github.com/better-auth/better-auth) | 69 |
216+
| 9 | [rocket.chat](https://github.com/RocketChat/Rocket.Chat) | 67 |
217+
| 10 | [typebot](https://github.com/baptisteArno/typebot.io) | 66 |
218+
219+
<!-- LEADERBOARD:END -->
220+
221+
See the [full leaderboard](https://www.react.doctor/leaderboard).
222+
200223
## Resources & Contributing Back
201224

202225
Want to try it out? Check out [the demo](https://react.doctor).
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import type { Metadata } from "next";
2+
import Link from "next/link";
3+
import { PERFECT_SCORE } from "@/constants";
4+
import { getDoctorFace } from "@/utils/get-doctor-face";
5+
import { getScoreColorClass } from "@/utils/get-score-color-class";
6+
7+
const SCORE_BAR_WIDTH = 20;
8+
const REVALIDATE_SECONDS = 60 * 60;
9+
const COMMAND = "npx -y react-doctor@latest .";
10+
const BENCHMARKS_REPO_URL = "https://github.com/millionco/react-doctor-benchmarks";
11+
const LEADERBOARD_URL =
12+
"https://raw.githubusercontent.com/millionco/react-doctor-benchmarks/main/results/leaderboard.json";
13+
const BOX_TOP = "\u250C\u2500\u2500\u2500\u2500\u2500\u2510";
14+
const BOX_BOTTOM = "\u2514\u2500\u2500\u2500\u2500\u2500\u2518";
15+
16+
interface LeaderboardEntry {
17+
slug: string;
18+
name: string;
19+
githubUrl: string;
20+
packageName: string;
21+
score: number;
22+
errorCount: number;
23+
warningCount: number;
24+
fileCount: number;
25+
commitSha: string;
26+
scannedAt: string;
27+
}
28+
29+
interface LeaderboardFile {
30+
schemaVersion: number;
31+
generatedAt: string;
32+
doctorVersion: string;
33+
source: { repo: string; path: string; docs: string };
34+
entries: LeaderboardEntry[];
35+
}
36+
37+
export const metadata: Metadata = {
38+
title: "Leaderboard - React Doctor",
39+
description:
40+
"Scores for popular open-source React projects, diagnosed by React Doctor. Updated automatically from public benchmarks.",
41+
};
42+
43+
const formatGeneratedAt = (isoTimestamp: string): string => {
44+
const parsedDate = new Date(isoTimestamp);
45+
if (Number.isNaN(parsedDate.getTime())) return isoTimestamp;
46+
return `${parsedDate.toISOString().replace("T", " ").slice(0, 16)} UTC`;
47+
};
48+
49+
const fetchLeaderboard = async (): Promise<LeaderboardFile | null> => {
50+
try {
51+
const response = await fetch(LEADERBOARD_URL, { next: { revalidate: REVALIDATE_SECONDS } });
52+
if (!response.ok) return null;
53+
return (await response.json()) as LeaderboardFile;
54+
} catch {
55+
return null;
56+
}
57+
};
58+
59+
const ScoreBar = ({ score }: { score: number }) => {
60+
const filledCount = Math.round((score / PERFECT_SCORE) * SCORE_BAR_WIDTH);
61+
const emptyCount = SCORE_BAR_WIDTH - filledCount;
62+
const colorClass = getScoreColorClass(score);
63+
64+
return (
65+
<span className="text-xs sm:text-sm">
66+
<span className={colorClass}>{"\u2588".repeat(filledCount)}</span>
67+
<span className="text-neutral-700">{"\u2591".repeat(emptyCount)}</span>
68+
</span>
69+
);
70+
};
71+
72+
const LeaderboardRow = ({ entry, rank }: { entry: LeaderboardEntry; rank: number }) => {
73+
const colorClass = getScoreColorClass(entry.score);
74+
75+
return (
76+
<div className="group grid grid-cols-[2rem_1fr_auto] items-center border-b border-white/5 py-2 transition-colors hover:bg-white/2 sm:grid-cols-[2.5rem_minmax(0,1fr)_auto_auto] sm:py-2.5">
77+
<span className="text-right text-neutral-600">{rank}</span>
78+
79+
<a
80+
href={entry.githubUrl}
81+
target="_blank"
82+
rel="noopener noreferrer"
83+
className="ml-2 truncate text-white transition-colors hover:text-blue-400 sm:ml-4"
84+
>
85+
{entry.name}
86+
<span className="ml-2 hidden text-sm text-neutral-500 sm:inline">{entry.packageName}</span>
87+
</a>
88+
89+
<span className="ml-4 hidden sm:inline">
90+
<ScoreBar score={entry.score} />
91+
</span>
92+
93+
<span className="ml-4 text-right">
94+
<span className={`${colorClass} font-medium`}>{entry.score}</span>
95+
<span className="text-neutral-600">/{PERFECT_SCORE}</span>
96+
</span>
97+
</div>
98+
);
99+
};
100+
101+
const LeaderboardPage = async () => {
102+
const leaderboard = await fetchLeaderboard();
103+
const sortedEntries = leaderboard
104+
? leaderboard.entries.toSorted((leftEntry, rightEntry) => rightEntry.score - leftEntry.score)
105+
: [];
106+
const topScore = sortedEntries[0]?.score ?? 0;
107+
const [eyes, mouth] = getDoctorFace(topScore);
108+
const topScoreColor = getScoreColorClass(topScore);
109+
110+
return (
111+
<div className="mx-auto min-h-screen w-full max-w-3xl bg-[#0a0a0a] p-6 pb-32 font-mono text-base leading-relaxed text-neutral-300 sm:p-8 sm:pb-40 sm:text-lg">
112+
<div className="mb-8">
113+
<Link
114+
href="/"
115+
className="inline-flex items-center gap-2 text-neutral-500 transition-colors hover:text-neutral-300"
116+
>
117+
<img src="/favicon.svg" alt="React Doctor" width={20} height={20} />
118+
<span>react-doctor</span>
119+
</Link>
120+
</div>
121+
122+
{leaderboard && (
123+
<div className="mb-2">
124+
<pre className={`${topScoreColor} leading-tight`}>
125+
{` ${BOX_TOP}\n \u2502 ${eyes} \u2502\n \u2502 ${mouth} \u2502\n ${BOX_BOTTOM}`}
126+
</pre>
127+
</div>
128+
)}
129+
130+
<div className="mb-1 text-xl text-white">Leaderboard</div>
131+
<div className="mb-2 text-neutral-500">Scores for popular open-source React projects.</div>
132+
133+
{leaderboard && (
134+
<div className="mb-8 text-sm text-neutral-600">
135+
{sortedEntries.length} repos scanned with v{leaderboard.doctorVersion} on{" "}
136+
{formatGeneratedAt(leaderboard.generatedAt)}.
137+
</div>
138+
)}
139+
140+
{leaderboard ? (
141+
<div className="mb-8">
142+
{sortedEntries.map((entry, innerIndex) => (
143+
<LeaderboardRow key={entry.slug} entry={entry} rank={innerIndex + 1} />
144+
))}
145+
</div>
146+
) : (
147+
<div className="mb-8 text-red-400">
148+
Could not load the leaderboard right now. Check{" "}
149+
<a
150+
href={BENCHMARKS_REPO_URL}
151+
target="_blank"
152+
rel="noopener noreferrer"
153+
className="underline underline-offset-2 hover:text-red-300"
154+
>
155+
the benchmarks repo
156+
</a>
157+
.
158+
</div>
159+
)}
160+
161+
<div className="min-h-[1.4em]" />
162+
163+
<div className="text-neutral-500">Run it on your codebase:</div>
164+
<div className="mt-2">
165+
<span className="border border-white/20 px-3 py-1.5 text-white">{COMMAND}</span>
166+
</div>
167+
168+
<div className="min-h-[1.4em]" />
169+
<div className="min-h-[1.4em]" />
170+
171+
<div className="text-neutral-500">
172+
{"+ "}
173+
<a
174+
href={BENCHMARKS_REPO_URL}
175+
target="_blank"
176+
rel="noopener noreferrer"
177+
className="text-green-400 transition-colors hover:text-green-300 hover:underline"
178+
>
179+
Add your project
180+
</a>
181+
<span className="text-neutral-600">{" - open a PR on react-doctor-benchmarks"}</span>
182+
</div>
183+
</div>
184+
);
185+
};
186+
187+
export default LeaderboardPage;

packages/website/src/app/share/animated-score.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,16 @@
11
"use client";
22

33
import { useEffect, useState } from "react";
4+
import { PERFECT_SCORE } from "@/constants";
5+
import { getScoreColorClass } from "@/utils/get-score-color-class";
6+
import { getScoreLabel } from "@/utils/get-score-label";
47

5-
const PERFECT_SCORE = 100;
6-
const SCORE_GOOD_THRESHOLD = 75;
7-
const SCORE_OK_THRESHOLD = 50;
88
const SCORE_BAR_WIDTH = 30;
99
const SCORE_FRAME_COUNT = 20;
1010
const SCORE_FRAME_DELAY_MS = 30;
1111

1212
const easeOutCubic = (progress: number) => 1 - Math.pow(1 - progress, 3);
1313

14-
const getScoreColorClass = (score: number): string => {
15-
if (score >= SCORE_GOOD_THRESHOLD) return "text-green-400";
16-
if (score >= SCORE_OK_THRESHOLD) return "text-yellow-500";
17-
return "text-red-400";
18-
};
19-
20-
const getScoreLabel = (score: number): string => {
21-
if (score >= SCORE_GOOD_THRESHOLD) return "Great";
22-
if (score >= SCORE_OK_THRESHOLD) return "Needs work";
23-
return "Critical";
24-
};
25-
2614
const ScoreBar = ({ score }: { score: number }) => {
2715
const filledCount = Math.round((score / PERFECT_SCORE) * SCORE_BAR_WIDTH);
2816
const emptyCount = SCORE_BAR_WIDTH - filledCount;

packages/website/src/app/share/page.tsx

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { Metadata } from "next";
2+
import { PERFECT_SCORE } from "@/constants";
3+
import { getDoctorFace } from "@/utils/get-doctor-face";
4+
import { getScoreColorClass } from "@/utils/get-score-color-class";
5+
import { getScoreLabel } from "@/utils/get-score-label";
26
import AnimatedScore from "./animated-score";
37
import BadgeSnippet from "./badge-snippet";
48

5-
const PERFECT_SCORE = 100;
6-
const SCORE_GOOD_THRESHOLD = 75;
7-
const SCORE_OK_THRESHOLD = 50;
89
const MAX_PROJECT_NAME_LENGTH = 100;
910
const MAX_DISPLAY_COUNT = 99_999;
1011
const COMMAND = "npx -y react-doctor@latest .";
@@ -30,24 +31,6 @@ const clampProjectName = (value: string | undefined | null): string | null => {
3031
return value.length > MAX_PROJECT_NAME_LENGTH ? value.slice(0, MAX_PROJECT_NAME_LENGTH) : value;
3132
};
3233

33-
const getScoreLabel = (score: number): string => {
34-
if (score >= SCORE_GOOD_THRESHOLD) return "Great";
35-
if (score >= SCORE_OK_THRESHOLD) return "Needs work";
36-
return "Critical";
37-
};
38-
39-
const getScoreColorClass = (score: number): string => {
40-
if (score >= SCORE_GOOD_THRESHOLD) return "text-green-400";
41-
if (score >= SCORE_OK_THRESHOLD) return "text-yellow-500";
42-
return "text-red-400";
43-
};
44-
45-
const getDoctorFace = (score: number): [string, string] => {
46-
if (score >= SCORE_GOOD_THRESHOLD) return ["\u25E0 \u25E0", " \u25BD "];
47-
if (score >= SCORE_OK_THRESHOLD) return ["\u2022 \u2022", " \u2500 "];
48-
return ["x x", " \u25BD "];
49-
};
50-
5134
const DoctorFace = ({ score }: { score: number }) => {
5235
const [eyes, mouth] = getDoctorFace(score);
5336
const colorClass = getScoreColorClass(score);

0 commit comments

Comments
 (0)