diff --git a/.github/workflows/ci-next-js.yml b/.github/workflows/ci-next-js.yml index 00ea05d..4157012 100644 --- a/.github/workflows/ci-next-js.yml +++ b/.github/workflows/ci-next-js.yml @@ -21,7 +21,7 @@ on: - 'prettier.config.js' jobs: - build: + test: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/ci-react-router.yml b/.github/workflows/ci-react-router.yml index 937c955..9200f0d 100644 --- a/.github/workflows/ci-react-router.yml +++ b/.github/workflows/ci-react-router.yml @@ -21,7 +21,7 @@ on: - 'prettier.config.js' jobs: - build: + test: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/ci-tanstack-start.yml b/.github/workflows/ci-tanstack-start.yml index 1714cf8..bcfdcb6 100644 --- a/.github/workflows/ci-tanstack-start.yml +++ b/.github/workflows/ci-tanstack-start.yml @@ -21,7 +21,7 @@ on: - 'prettier.config.js' jobs: - build: + test: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/generate-stats.yml b/.github/workflows/generate-stats.yml index 84acade..479369a 100644 --- a/.github/workflows/generate-stats.yml +++ b/.github/workflows/generate-stats.yml @@ -22,13 +22,184 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 - - name: Setup Node.js + - name: Setup Node.js (no cache for clean measurements) uses: actions/setup-node@v4 with: node-version: '24' - cache: 'pnpm' + # No cache - we want clean measurements - - name: Install dependencies + - name: Detect changed packages + id: detect + run: | + # Check which packages changed in the last commit + PACKAGES="" + if git diff --name-only HEAD~1 HEAD | grep -q "packages/starter-next-js/"; then + PACKAGES="$PACKAGES next" + fi + if git diff --name-only HEAD~1 HEAD | grep -q "packages/starter-react-router/"; then + PACKAGES="$PACKAGES react-router" + fi + if git diff --name-only HEAD~1 HEAD | grep -q "packages/starter-tanstack-start-react/"; then + PACKAGES="$PACKAGES tanstack" + fi + if git diff --name-only HEAD~1 HEAD | grep -q "packages/starter-nuxt/"; then + PACKAGES="$PACKAGES nuxt" + fi + + # If no packages changed but package.json changed, measure all + if [ -z "$PACKAGES" ] && git diff --name-only HEAD~1 HEAD | grep -q "package.json"; then + PACKAGES="next react-router tanstack nuxt" + fi + + echo "packages=$PACKAGES" >> $GITHUB_OUTPUT + echo "Changed packages: $PACKAGES" + + - name: Measure install time (clean, no cache) + id: install + if: steps.detect.outputs.packages != '' + run: | + START=$(date +%s%N) + pnpm install --frozen-lockfile + END=$(date +%s%N) + ELAPSED=$((($END - $START) / 1000000)) + echo "time=$ELAPSED" >> $GITHUB_OUTPUT + echo "Install time: ${ELAPSED}ms" + + - name: Measure Next.js builds + if: contains(steps.detect.outputs.packages, 'next') + id: next + run: | + # Cold build + START=$(date +%s%N) + pnpm build:next + END=$(date +%s%N) + COLD=$((($END - $START) / 1000000)) + + # Warm build + START=$(date +%s%N) + pnpm build:next + END=$(date +%s%N) + WARM=$((($END - $START) / 1000000)) + + echo "cold=$COLD" >> $GITHUB_OUTPUT + echo "warm=$WARM" >> $GITHUB_OUTPUT + echo "Next.js - Cold: ${COLD}ms, Warm: ${WARM}ms" + + - name: Measure React Router builds + if: contains(steps.detect.outputs.packages, 'react-router') + id: react_router + run: | + # Cold build + START=$(date +%s%N) + pnpm build:react-router + END=$(date +%s%N) + COLD=$((($END - $START) / 1000000)) + + # Warm build + START=$(date +%s%N) + pnpm build:react-router + END=$(date +%s%N) + WARM=$((($END - $START) / 1000000)) + + echo "cold=$COLD" >> $GITHUB_OUTPUT + echo "warm=$WARM" >> $GITHUB_OUTPUT + echo "React Router - Cold: ${COLD}ms, Warm: ${WARM}ms" + + - name: Measure TanStack Start builds + if: contains(steps.detect.outputs.packages, 'tanstack') + id: tanstack + run: | + # Cold build + START=$(date +%s%N) + pnpm build:tanstack + END=$(date +%s%N) + COLD=$((($END - $START) / 1000000)) + + # Warm build + START=$(date +%s%N) + pnpm build:tanstack + END=$(date +%s%N) + WARM=$((($END - $START) / 1000000)) + + echo "cold=$COLD" >> $GITHUB_OUTPUT + echo "warm=$WARM" >> $GITHUB_OUTPUT + echo "TanStack Start - Cold: ${COLD}ms, Warm: ${WARM}ms" + + - name: Measure Nuxt builds + if: contains(steps.detect.outputs.packages, 'nuxt') + id: nuxt + run: | + # Cold build + START=$(date +%s%N) + pnpm build:nuxt + END=$(date +%s%N) + COLD=$((($END - $START) / 1000000)) + + # Warm build + START=$(date +%s%N) + pnpm build:nuxt + END=$(date +%s%N) + WARM=$((($END - $START) / 1000000)) + + echo "cold=$COLD" >> $GITHUB_OUTPUT + echo "warm=$WARM" >> $GITHUB_OUTPUT + echo "Nuxt - Cold: ${COLD}ms, Warm: ${WARM}ms" + + - name: Save all CI stats + if: steps.detect.outputs.packages != '' + run: | + TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) + + # Save Next.js stats + if [[ "${{ steps.detect.outputs.packages }}" == *"next"* ]]; then + mkdir -p packages/starter-next-js + echo '{ + "installTimeMs": ${{ steps.install.outputs.time }}, + "coldBuildTimeMs": ${{ steps.next.outputs.cold }}, + "warmBuildTimeMs": ${{ steps.next.outputs.warm }}, + "timingMeasuredAt": "'$TIMESTAMP'", + "runner": "ubuntu-latest" + }' > packages/starter-next-js/.ci-stats.json + fi + + # Save React Router stats + if [[ "${{ steps.detect.outputs.packages }}" == *"react-router"* ]]; then + mkdir -p packages/starter-react-router + echo '{ + "installTimeMs": ${{ steps.install.outputs.time }}, + "coldBuildTimeMs": ${{ steps.react_router.outputs.cold }}, + "warmBuildTimeMs": ${{ steps.react_router.outputs.warm }}, + "timingMeasuredAt": "'$TIMESTAMP'", + "runner": "ubuntu-latest" + }' > packages/starter-react-router/.ci-stats.json + fi + + # Save TanStack Start stats + if [[ "${{ steps.detect.outputs.packages }}" == *"tanstack"* ]]; then + mkdir -p packages/starter-tanstack-start-react + echo '{ + "installTimeMs": ${{ steps.install.outputs.time }}, + "coldBuildTimeMs": ${{ steps.tanstack.outputs.cold }}, + "warmBuildTimeMs": ${{ steps.tanstack.outputs.warm }}, + "timingMeasuredAt": "'$TIMESTAMP'", + "runner": "ubuntu-latest" + }' > packages/starter-tanstack-start-react/.ci-stats.json + fi + + # Save Nuxt stats + if [[ "${{ steps.detect.outputs.packages }}" == *"nuxt"* ]]; then + mkdir -p packages/starter-nuxt + echo '{ + "installTimeMs": ${{ steps.install.outputs.time }}, + "coldBuildTimeMs": ${{ steps.nuxt.outputs.cold }}, + "warmBuildTimeMs": ${{ steps.nuxt.outputs.warm }}, + "timingMeasuredAt": "'$TIMESTAMP'", + "runner": "ubuntu-latest" + }' > packages/starter-nuxt/.ci-stats.json + fi + + - name: Install dependencies (if not already installed) + if: steps.detect.outputs.packages == '' run: pnpm install --frozen-lockfile - name: Generate stats @@ -53,15 +224,19 @@ jobs: # Commit and push changes git add . - git commit -m "Update stats after starter changes" + git commit -m "Update CI stats and generated stats after starter changes" git push --force-with-lease origin automated-stats-update # Create PR if it doesn't exist if ! gh pr view automated-stats-update &>/dev/null; then gh pr create \ - --title "Update stats after starter changes" \ + --title "Update CI stats and generated stats after starter changes" \ --body "Automated stats update triggered by changes to starter packages. + Includes: + - CI build time measurements (.ci-stats.json) + - Generated framework comparison stats + This PR was automatically created and will auto-merge if all checks pass." \ --base main \ --head automated-stats-update diff --git a/packages/starter-next-js/.ci-stats.json b/packages/starter-next-js/.ci-stats.json new file mode 100644 index 0000000..e92f783 --- /dev/null +++ b/packages/starter-next-js/.ci-stats.json @@ -0,0 +1,7 @@ +{ + "installTimeMs": 10880, + "coldBuildTimeMs": 7559, + "warmBuildTimeMs": 7491, + "timingMeasuredAt": "2026-01-12T09:30:56Z", + "runner": "ubuntu-latest" +} diff --git a/packages/starter-next-js/package.json b/packages/starter-next-js/package.json index 5fea998..1ea60a7 100644 --- a/packages/starter-next-js/package.json +++ b/packages/starter-next-js/package.json @@ -19,8 +19,8 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", + "@types/node": "^25.0.6", + "@types/react": "^19.2.8", "@types/react-dom": "^19", "eslint-config-next": "16.1.1", "tailwindcss": "^4" diff --git a/packages/starter-react-router/.ci-stats.json b/packages/starter-react-router/.ci-stats.json new file mode 100644 index 0000000..90e9c05 --- /dev/null +++ b/packages/starter-react-router/.ci-stats.json @@ -0,0 +1,7 @@ +{ + "installTimeMs": 10880, + "coldBuildTimeMs": 3025, + "warmBuildTimeMs": 3019, + "timingMeasuredAt": "2026-01-12T09:30:56Z", + "runner": "ubuntu-latest" +} diff --git a/packages/starter-tanstack-start-react/.ci-stats.json b/packages/starter-tanstack-start-react/.ci-stats.json new file mode 100644 index 0000000..5913124 --- /dev/null +++ b/packages/starter-tanstack-start-react/.ci-stats.json @@ -0,0 +1,7 @@ +{ + "installTimeMs": 10880, + "coldBuildTimeMs": 9658, + "warmBuildTimeMs": 9355, + "timingMeasuredAt": "2026-01-12T09:30:56Z", + "runner": "ubuntu-latest" +} diff --git a/packages/stats-generator/src/create-stats.ts b/packages/stats-generator/src/create-stats.ts index cfe9962..2d57462 100644 --- a/packages/stats-generator/src/create-stats.ts +++ b/packages/stats-generator/src/create-stats.ts @@ -3,6 +3,7 @@ import { join } from 'node:path' import { getStarterPackages } from './get-starter-packages.ts' import { packagesDir } from './constants.ts' import { saveStats } from './save-stats.ts' +import { getCIStats } from './get-ci-stats.ts' import type { FrameworkStats, PackageJson } from './types.ts' async function createStats() { @@ -21,9 +22,12 @@ async function createStats() { const prodCount = Object.keys(dependencies).length const devCount = Object.keys(devDependencies).length + const ciStats = await getCIStats(pkgDir) + const stats: FrameworkStats = { prodDependencies: prodCount, devDependencies: devCount, + ...(ciStats ?? {}), } await saveStats(pkgDir, stats) diff --git a/packages/stats-generator/src/get-ci-stats.ts b/packages/stats-generator/src/get-ci-stats.ts new file mode 100644 index 0000000..dfbd050 --- /dev/null +++ b/packages/stats-generator/src/get-ci-stats.ts @@ -0,0 +1,15 @@ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { packagesDir } from './constants.ts' +import { CIStats } from './types.ts' + +export async function getCIStats(pkgDir: string) { + const ciStatsPath = join(packagesDir, pkgDir, '.ci-stats.json') + + try { + const content = await readFile(ciStatsPath, 'utf-8') + return JSON.parse(content) as CIStats + } catch (error) { + return null + } +} diff --git a/packages/stats-generator/src/types.ts b/packages/stats-generator/src/types.ts index e2f3897..4c0760c 100644 --- a/packages/stats-generator/src/types.ts +++ b/packages/stats-generator/src/types.ts @@ -1,4 +1,11 @@ -export interface FrameworkStats { +export interface CIStats { + installTimeMs?: number + coldBuildTimeMs?: number + warmBuildTimeMs?: number + timingMeasuredAt?: string +} + +export interface FrameworkStats extends CIStats { prodDependencies: number devDependencies: number } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0116a0f..2115770 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,7 +31,7 @@ importers: dependencies: astro: specifier: ^5.16.6 - version: 5.16.6(@types/node@25.0.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 5.16.6(@types/node@25.0.6)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) devDependencies: '@astrojs/check': specifier: ^0.9.6 @@ -56,14 +56,14 @@ importers: specifier: ^4 version: 4.1.18 '@types/node': - specifier: ^20 - version: 20.19.27 + specifier: ^25.0.6 + version: 25.0.6 '@types/react': - specifier: ^19 - version: 19.2.7 + specifier: ^19.2.8 + version: 19.2.8 '@types/react-dom': specifier: ^19 - version: 19.2.3(@types/react@19.2.7) + version: 19.2.3(@types/react@19.2.8) eslint-config-next: specifier: 16.1.1 version: 16.1.1(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) @@ -2457,15 +2457,15 @@ packages: '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} - '@types/node@20.19.27': - resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} - '@types/node@22.19.3': resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + '@types/node@25.0.6': + resolution: {integrity: sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==} + '@types/parse-path@7.1.0': resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==} deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed. @@ -2478,6 +2478,9 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/react@19.2.8': + resolution: {integrity: sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==} + '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -9445,7 +9448,7 @@ snapshots: '@types/fontkit@2.0.8': dependencies: - '@types/node': 22.19.3 + '@types/node': 25.0.6 '@types/hast@3.0.4': dependencies: @@ -9465,10 +9468,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/node@20.19.27': - dependencies: - undici-types: 6.21.0 - '@types/node@22.19.3': dependencies: undici-types: 6.21.0 @@ -9477,6 +9476,10 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/node@25.0.6': + dependencies: + undici-types: 7.16.0 + '@types/parse-path@7.1.0': dependencies: parse-path: 7.1.0 @@ -9485,10 +9488,18 @@ snapshots: dependencies: '@types/react': 19.2.7 + '@types/react-dom@19.2.3(@types/react@19.2.8)': + dependencies: + '@types/react': 19.2.8 + '@types/react@19.2.7': dependencies: csstype: 3.2.3 + '@types/react@19.2.8': + dependencies: + csstype: 3.2.3 + '@types/resolve@1.20.2': {} '@types/unist@3.0.3': {} @@ -10160,7 +10171,7 @@ snapshots: transitivePeerDependencies: - supports-color - astro@5.16.6(@types/node@25.0.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + astro@5.16.6(@types/node@25.0.6)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): dependencies: '@astrojs/compiler': 2.13.0 '@astrojs/internal-helpers': 0.7.5 @@ -10217,8 +10228,8 @@ snapshots: unist-util-visit: 5.0.0 unstorage: 1.17.3(db0@0.3.4)(ioredis@5.8.2) vfile: 6.0.3 - vite: 6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitefu: 1.1.1(vite@6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + vite: 6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) xxhash-wasm: 1.1.0 yargs-parser: 21.1.1 yocto-spinner: 0.2.3 @@ -14926,7 +14937,7 @@ snapshots: - supports-color - typescript - vite@6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite@6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -14935,7 +14946,7 @@ snapshots: rollup: 4.54.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.3 + '@types/node': 25.0.6 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 @@ -14960,9 +14971,9 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitefu@1.1.1(vite@6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vitefu@1.1.1(vite@6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): optionalDependencies: - vite: 6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitefu@1.1.1(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): optionalDependencies: