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';

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+`;
}
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;
});
18 changes: 18 additions & 0 deletions .github/scripts/tsconfig.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.

Is this file actually needed? The root tsconfig.json already enables the same options

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.

Root tsconfig.json isn’t a good fit because it lacks DOM lib (needed for fetch) and would typecheck too much of the repo. I moved the minimal config to .github/scripts/tsconfig.json (since it’s for CI scripts) and updated the docs check script to use it; removed docs/scripts/tsconfig.json, will push the commit changes shortly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Root tsconfig.json isn’t a good fit because it lacks DOM lib (needed for fetch)

This script shouldn't rely on DOM lib since it'll be executing in a node env.

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.

Root tsconfig.json isn’t a good fit because it lacks DOM lib (needed for fetch)

This script shouldn't rely on DOM lib since it'll be executing in a node env.

Good point — agreed. Even though the script uses fetch, it runs in Node, so it shouldn’t depend on the DOM lib.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2024",
"lib": ["ES2024"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"typeRoots": ["../../docs/node_modules/@types"],
"types": ["node"],
"verbatimModuleSyntax": true
},
"include": [
"./fetch-trusted-stack-stats.ts",
"../../docs/.vitepress/theme/data/trusted-stack-stats.types.ts"
]
}
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"]
}
8 changes: 6 additions & 2 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,9 @@
"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",
"update-trusted-stack-stats:check": "pnpm exec tsc -p ../.github/scripts/tsconfig.json --noEmit"
},
"dependencies": {
"@iconify/vue": "^5.0.0",
Expand All @@ -18,10 +20,12 @@
"vue3-carousel": "^0.16.0"
},
"devDependencies": {
"@types/node": "24.10.3",
"@voidzero-dev/vitepress-theme": "4.8.3",
"oxc-minify": "^0.120.0",
"tailwindcss": "^4.1.18",
"typescript": "^6.0.0",
"vitepress": "2.0.0-alpha.15"
},
"packageManager": "pnpm@10.33.0"
}
}
Loading