diff --git a/.github/scripts/fetch-trusted-stack-stats.ts b/.github/scripts/fetch-trusted-stack-stats.ts new file mode 100644 index 0000000000..4c8e0fff41 --- /dev/null +++ b/.github/scripts/fetch-trusted-stack-stats.ts @@ -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 { + 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 { + const url = `https://api.github.com/repos/${repo}`; + const headers: Record = { + 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 { + 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; +}); diff --git a/.github/workflows/update-trusted-stack-stats.yml b/.github/workflows/update-trusted-stack-stats.yml new file mode 100644 index 0000000000..53b7cafb7e --- /dev/null +++ b/.github/workflows/update-trusted-stack-stats.yml @@ -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 diff --git a/docs/.vitepress/theme/components/home/FeatureCheck.vue b/docs/.vitepress/theme/components/home/FeatureCheck.vue index ef5f36a95d..5abdf242c8 100644 --- a/docs/.vitepress/theme/components/home/FeatureCheck.vue +++ b/docs/.vitepress/theme/components/home/FeatureCheck.vue @@ -18,7 +18,7 @@ import oxcIcon from '@assets/icons/oxc-light.svg'; formatting
  • - 600+ ESLint compatible + 750+ ESLint compatible rules
  • diff --git a/docs/.vitepress/theme/components/home/ProductivityGrid.vue b/docs/.vitepress/theme/components/home/ProductivityGrid.vue index e747139e65..3e14266e1b 100644 --- a/docs/.vitepress/theme/components/home/ProductivityGrid.vue +++ b/docs/.vitepress/theme/components/home/ProductivityGrid.vue @@ -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');