Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .github/scripts/fetch-trusted-stack-stats.ts
Comment thread
camc314 marked this conversation as resolved.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this script live under .github/scripts/?
It's CI-only and uses the same peter-evans/create-pull-request pattern as upgrade-deps.yml.

Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Fetches last-week npm download counts and GitHub star counts, then writes
* docs/.vitepress/theme/data/trusted-stack-stats.json for the docs home page.
*
* Requires Node.js >=22.18 (strip types). Run:
* `pnpm -C docs update-trusted-stack-stats`
* or: `node .github/scripts/fetch-trusted-stack-stats.ts`
*/
import { writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import type {
TrustedStackProjectId,
TrustedStackStatProject,
TrustedStackStatsFile,
} from '../../docs/.vitepress/theme/data/trusted-stack-stats.types.ts';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think fetch-trusted-stack-stats.ts belongs under .github/, but I'm a bit uneasy that types defined in docs/ end up imported into .github/.

@fengmk2 @camc314 WDYT?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we place fetch-trusted-stack-stats.ts in docs/.vitepress/theme/data/? This way it's also possible to run this script during local development.

Copy link
Copy Markdown
Collaborator

@jong-kyung jong-kyung May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fengmk2 That makes sense — I had been thinking along the lines of co-locating it with the workflow since it's CI-driven, but keeping it next to the JSON it produces is a cleaner home. Thanks☺️


const __dirname = dirname(fileURLToPath(import.meta.url));
const OUT = join(__dirname, '../../docs/.vitepress/theme/data/trusted-stack-stats.json');

interface ProjectSource {
readonly id: TrustedStackProjectId;
readonly npmPackage: string;
readonly githubRepo: string;
}

const PROJECTS: readonly ProjectSource[] = [
{ id: 'vite', npmPackage: 'vite', githubRepo: 'vitejs/vite' },
{ id: 'vitest', npmPackage: 'vitest', githubRepo: 'vitest-dev/vitest' },
/** OXC row uses `oxlint` npm weekly downloads as a concrete proxy for the Oxc toolchain. */
{ id: 'oxc', npmPackage: 'oxlint', githubRepo: 'oxc-project/oxc' },
];

function formatWeeklyDownloads(n: number): string {
if (n >= 10_000_000) {
return `${Math.round(n / 1e6)}m+`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid rounding npm downloads up before adding “+”

Using Math.round here can overstate the displayed metric because the m+ suffix reads as a lower bound; for example, 56,727,793 becomes 57m+ even though it is below 57 million. This introduces a recurring accuracy error in the homepage stats and can make values jump up/down around half-million boundaries on weekly refreshes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about using Math.floor here so the + suffix stays an honest lower bound?

}
const m = n / 1e6;
const s = m.toFixed(1).replace(/\.0$/, '');
return `${s}m+`;
}

function formatStars(s: number): string {
return `${(s / 1000).toFixed(1)}k`;
}

function parseNpmDownloadsJson(data: unknown, pkg: string): number {
if (typeof data !== 'object' || data === null || !('downloads' in data)) {
throw new Error(`npm API ${pkg}: unexpected payload`);
}
const downloads = (data as { downloads: unknown }).downloads;
if (typeof downloads !== 'number') {
throw new Error(`npm API ${pkg}: unexpected payload`);
}
return downloads;
}

async function npmLastWeekDownloads(pkg: string): Promise<number> {
const url = `https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(pkg)}`;
const res = await fetch(url);
if (!res.ok) {
const body = await res.text();
throw new Error(`npm API ${pkg}: HTTP ${res.status} ${body}`);
}
return parseNpmDownloadsJson(await res.json(), pkg);
}

function parseGithubRepoJson(data: unknown, repo: string): number {
if (typeof data !== 'object' || data === null || !('stargazers_count' in data)) {
throw new Error(`GitHub API ${repo}: unexpected payload`);
}
const count = (data as { stargazers_count: unknown }).stargazers_count;
if (typeof count !== 'number') {
throw new Error(`GitHub API ${repo}: unexpected payload`);
}
return count;
}

async function fetchGithubStargazers(repo: string): Promise<number> {
const url = `https://api.github.com/repos/${repo}`;
const headers: Record<string, string> = {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'voidzero-dev/vite-plus (.github/scripts/fetch-trusted-stack-stats.ts)',
};
const token = process.env.GITHUB_TOKEN;
if (token !== undefined && token !== '') {
headers.Authorization = `Bearer ${token}`;
}
const res = await fetch(url, { headers });
if (!res.ok) {
const body = await res.text();
throw new Error(`GitHub API ${repo}: HTTP ${res.status} ${body}`);
}
return parseGithubRepoJson(await res.json(), repo);
}

async function main(): Promise<void> {
const projects: TrustedStackStatProject[] = [];
for (const p of PROJECTS) {
const [npmWeeklyDownloads, stars] = await Promise.all([
npmLastWeekDownloads(p.npmPackage),
fetchGithubStargazers(p.githubRepo),
]);
const row: TrustedStackStatProject = {
id: p.id,
npmPackage: p.npmPackage,
githubRepo: p.githubRepo,
npmWeeklyDownloads,
githubStargazers: stars,
npmWeeklyDownloadsDisplay: formatWeeklyDownloads(npmWeeklyDownloads),
githubStarsDisplay: formatStars(stars),
};
projects.push(row);
}
const payload: TrustedStackStatsFile = { projects };
await writeFile(OUT, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
console.log(`Wrote ${OUT} at ${new Date().toISOString()}`);
}

void main().catch((err: unknown) => {
console.error(err);
process.exitCode = 1;
});
48 changes: 48 additions & 0 deletions .github/workflows/update-trusted-stack-stats.yml
Comment thread
camc314 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Update trusted stack stats

on:
schedule:
# Weekly: Monday 06:00 UTC
- cron: '0 6 * * 1'
workflow_dispatch:

defaults:
run:
shell: bash

jobs:
update:
if: github.repository == 'voidzero-dev/vite-plus' && github.event.repository.fork == false
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: .node-version

- name: Fetch npm and GitHub stats
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node .github/scripts/fetch-trusted-stack-stats.ts

- name: Create or update PR
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
with:
base: main
branch: chore/docs-trusted-stack-stats
title: 'chore(docs): refresh trusted stack stats'
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore(docs): refresh trusted stack stats'
add-paths: |
docs/.vitepress/theme/data/trusted-stack-stats.json
body: |
Automated update of trusted stack statistics on the docs homepage.

- npm weekly downloads (last-week): vite, vitest, oxlint
- GitHub stars: vitejs/vite, vitest-dev/vitest, oxc-project/oxc
2 changes: 1 addition & 1 deletion docs/.vitepress/theme/components/home/FeatureCheck.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import oxcIcon from '@assets/icons/oxc-light.svg';
formatting
</li>
<li>
600+ <code class="mx-1 outline-none bg-nickel/50 text-aqua">ESLint</code> compatible
750+ <code class="mx-1 outline-none bg-nickel/50 text-aqua">ESLint</code> compatible
rules
</li>
<li>
Expand Down
29 changes: 23 additions & 6 deletions docs/.vitepress/theme/components/home/ProductivityGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import productivitySecurityImage from '@local-assets/productivity-security.png';
import tileOxc from '@local-assets/tiles/oxc.png';
import tileVite from '@local-assets/tiles/vite.png';
import tileVitest from '@local-assets/tiles/vitest.png';
import { trustedStackById } from '../../data/trusted-stack-stats';

const viteStack = trustedStackById('vite');
const vitestStack = trustedStackById('vitest');
const oxcStack = trustedStackById('oxc');
</script>

<template>
Expand All @@ -31,13 +36,17 @@ import tileVitest from '@local-assets/tiles/vitest.png';
<div
class="relative flex flex-col flex-1 sm:pl-6 before:content-none sm:before:content-[''] before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:h-full before:w-px before:bg-stroke"
>
<p class="text-primary font-medium text-base sm:text-xl">69m+</p>
<p class="text-primary font-medium text-base sm:text-xl">
{{ viteStack.npmWeeklyDownloadsDisplay }}
</p>
<p class="leading-tight text-base">Weekly npm downloads</p>
</div>
<div
class="relative flex flex-col flex-shrink-0 pl-6 before:content-[''] before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:h-full before:w-px before:bg-stroke"
>
<p class="text-primary font-medium text-base sm:text-xl">78.7k</p>
<p class="text-primary font-medium text-base sm:text-xl">
{{ viteStack.githubStarsDisplay }}
</p>
<p class="leading-tight text-base">GitHub stars</p>
</div>
</div>
Expand All @@ -54,13 +63,17 @@ import tileVitest from '@local-assets/tiles/vitest.png';
<div
class="relative flex flex-col flex-1 sm:pl-6 before:content-none sm:before:content-[''] before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:h-full before:w-px before:bg-stroke"
>
<p class="text-primary font-medium text-base sm:text-xl">35m+</p>
<p class="text-primary font-medium text-base sm:text-xl">
{{ vitestStack.npmWeeklyDownloadsDisplay }}
</p>
<p class="leading-tight text-base">Weekly npm downloads</p>
</div>
<div
class="relative flex flex-col flex-shrink-0 pl-6 before:content-[''] before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:h-full before:w-px before:bg-stroke"
>
<p class="text-primary font-medium text-base sm:text-xl">16.1k</p>
<p class="text-primary font-medium text-base sm:text-xl">
{{ vitestStack.githubStarsDisplay }}
</p>
<p class="leading-tight text-base">GitHub stars</p>
</div>
</div>
Expand All @@ -77,13 +90,17 @@ import tileVitest from '@local-assets/tiles/vitest.png';
<div
class="relative flex flex-col flex-1 sm:pl-6 before:content-none sm:before:content-[''] before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:h-full before:w-px before:bg-stroke"
>
<p class="text-primary font-medium text-base sm:text-xl">5m+</p>
<p class="text-primary font-medium text-base sm:text-xl">
{{ oxcStack.npmWeeklyDownloadsDisplay }}
</p>
<p class="leading-tight text-base">Weekly npm downloads</p>
</div>
<div
class="relative flex flex-col flex-shrink-0 pl-6 before:content-[''] before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:h-full before:w-px before:bg-stroke"
>
<p class="text-primary font-medium text-base sm:text-xl">19.8k</p>
<p class="text-primary font-medium text-base sm:text-xl">
{{ oxcStack.githubStarsDisplay }}
</p>
<p class="leading-tight text-base">GitHub stars</p>
</div>
</div>
Expand Down
31 changes: 31 additions & 0 deletions docs/.vitepress/theme/data/trusted-stack-stats.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"projects": [
{
"id": "vite",
"npmPackage": "vite",
"githubRepo": "vitejs/vite",
"npmWeeklyDownloads": 114052837,
"githubStargazers": 80401,
"npmWeeklyDownloadsDisplay": "114m+",
"githubStarsDisplay": "80.4k"
},
{
"id": "vitest",
"npmPackage": "vitest",
"githubRepo": "vitest-dev/vitest",
"npmWeeklyDownloads": 56727793,
"githubStargazers": 16471,
"npmWeeklyDownloadsDisplay": "57m+",
"githubStarsDisplay": "16.5k"
},
{
"id": "oxc",
"npmPackage": "oxlint",
"githubRepo": "oxc-project/oxc",
"npmWeeklyDownloads": 5237088,
"githubStargazers": 20981,
"npmWeeklyDownloadsDisplay": "5.2m+",
"githubStarsDisplay": "21.0k"
}
]
}
15 changes: 15 additions & 0 deletions docs/.vitepress/theme/data/trusted-stack-stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import raw from './trusted-stack-stats.json';

import type { TrustedStackProjectId, TrustedStackStatProject, TrustedStackStatsFile } from './trusted-stack-stats.types';

export type { TrustedStackProjectId, TrustedStackStatProject, TrustedStackStatsFile } from './trusted-stack-stats.types';

export const trustedStackStats = raw as TrustedStackStatsFile;

export function trustedStackById(id: TrustedStackProjectId): TrustedStackStatProject {
const project = trustedStackStats.projects.find((p) => p.id === id);
if (!project) {
throw new Error(`trusted-stack-stats.json: missing project "${id}"`);
}
return project;
}
15 changes: 15 additions & 0 deletions docs/.vitepress/theme/data/trusted-stack-stats.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type TrustedStackProjectId = 'vite' | 'vitest' | 'oxc';

export interface TrustedStackStatProject {
id: TrustedStackProjectId;
npmPackage: string;
githubRepo: string;
npmWeeklyDownloads: number;
githubStargazers: number;
npmWeeklyDownloadsDisplay: string;
githubStarsDisplay: string;
}

export interface TrustedStackStatsFile {
projects: TrustedStackStatProject[];
}
2 changes: 1 addition & 1 deletion docs/.vitepress/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
"@components/*": ["../node_modules/@voidzero-dev/vitepress-theme/src/components/*"]
}
},
"include": ["**/*.ts", "**/*.d.ts", "**/*.vue"]
"include": ["**/*.ts", "**/*.d.ts", "**/*.vue", "theme/data/*.json"]
}
7 changes: 4 additions & 3 deletions docs/package.json
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these two new devDependencies actually needed here? @types/node and typescript are already provided by the workspace root.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into this, and with our current setup it’s not really safe to remove those dependencies.

Even though typescript and @types/node exist at the workspace root, the docs/ directory is installed and locked independently (pnpm -C docs install --frozen-lockfile with its own docs/pnpm-lock.yaml). That means docs/ won’t reliably inherit root devDependencies unless we switch to a full workspace-level install.

Since docs/ also runs its own TypeScript check (update-trusted-stack-stats:check), it needs local access to tsc plus Node typings for imports and globals.

So for now, keeping typescript and @types/node in docs/package.json makes the most sense — it keeps docs/ self-contained and avoids CI or isolated install issues.

If we want to remove that duplication later, we’d first need to change the install/CI flow to rely on a root workspace install rather than pnpm -C docs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify further: the :check script isn't wired into any CI job, so it's not actually catching anything.

Could we drop:

  • docs/scripts/tsconfig.json
  • The update-trusted-stack-stats(:check) scripts in docs + root
  • @types/node and typescript from docs/devDependencies

cc. @camc314 WDYT?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — let’s see what @camc314 thinks first, then we can figure out the best path forward from there.
Happy to help once we’ve got their input.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"scripts": {
"dev": "vitepress dev",
"build": "cp ../packages/cli/install.sh ../packages/cli/install.ps1 public/ && vitepress build",
"preview": "vitepress preview"
"preview": "vitepress preview",
"update-trusted-stack-stats": "node ../.github/scripts/fetch-trusted-stack-stats.ts"
},
"dependencies": {
"@iconify/vue": "^5.0.0",
Expand All @@ -23,5 +24,5 @@
"tailwindcss": "^4.1.18",
"vitepress": "2.0.0-alpha.15"
},
"packageManager": "pnpm@10.33.2"
}
"packageManager": "pnpm@10.33.0"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you realign this back to pnpm@10.33.2?

}
Loading