diff --git a/.github/actions/setup-node-pnpm/action.yml b/.github/actions/setup-node-pnpm/action.yml new file mode 100644 index 0000000000..c256324553 --- /dev/null +++ b/.github/actions/setup-node-pnpm/action.yml @@ -0,0 +1,27 @@ +name: Setup Node.js and pnpm +description: Install pnpm, set up Node.js with cache, and install dependencies. Requires the repo to already be checked out. + +inputs: + node-version: + description: Node.js version to use + required: false + default: "24" + registry-url: + description: npm registry URL (e.g. https://registry.npmjs.org). Leave empty to skip. + required: false + default: "" + +runs: + using: composite + steps: + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: pnpm + registry-url: ${{ inputs.registry-url }} + + - name: Install dependencies + shell: bash + run: pnpm install --frozen-lockfile diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 4f216c5b7f..be6cbf495d 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -20,12 +20,10 @@ jobs: with: fetch-depth: 0 # Need full history for commit info - - uses: pnpm/action-setup@v4 + - uses: ./.github/actions/setup-node-pnpm - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm + - name: Build vinext plugin + run: pnpm run build - name: Install hyperfine run: | @@ -33,11 +31,6 @@ jobs: echo "5ffe6996aefbbf547f383116eea53569263cc7cf6598ee53f0081e37b702055d hyperfine_1.19.0_amd64.deb" | sha256sum -c sudo dpkg -i hyperfine_1.19.0_amd64.deb - # Install monorepo deps (builds vinext plugin via pnpm run build) - - run: pnpm install --frozen-lockfile - - name: Build vinext plugin - run: pnpm run build - # Generate the shared benchmark app (copies app/ into each project dir) - name: Generate benchmark app run: node benchmarks/generate-app.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55f35e30ff..d9f2c1cb8c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,12 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup-node-pnpm - run: pnpm run lint typecheck: @@ -33,12 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup-node-pnpm - run: pnpm run typecheck test: @@ -46,12 +36,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup-node-pnpm - name: Build plugin (needed by ecosystem fixtures) run: pnpm run build - run: pnpm test @@ -65,67 +50,46 @@ jobs: os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup-node-pnpm - name: Build plugin run: pnpm run build + - name: Pack vinext for local install run: npm pack --pack-destination "${{ runner.temp }}" working-directory: packages/vinext + - name: Scaffold a fresh create-next-app project run: npx create-next-app@latest "${{ runner.temp }}/cna-test" --yes + - name: Install vinext from local tarball working-directory: ${{ runner.temp }}/cna-test shell: bash run: npm install "${{ runner.temp }}"/vinext-*.tgz + - name: Run vinext init working-directory: ${{ runner.temp }}/cna-test run: npx vinext init --skip-check + - name: Start dev server and verify HTTP 200 working-directory: ${{ runner.temp }}/cna-test - shell: node {0} + shell: bash run: | - const { spawn } = require("child_process"); - const http = require("http"); - - const isWin = process.platform === "win32"; - const server = spawn("npx", ["vite", "dev", "--port", "3099"], { - cwd: process.cwd(), - stdio: "inherit", - shell: true, - }); - - let attempts = 0; - const maxAttempts = 30; - const interval = setInterval(() => { - attempts++; - http.get("http://localhost:3099/", (res) => { - if (res.statusCode === 200) { - console.log(`Server responded with HTTP 200 (attempt ${attempts})`); - clearInterval(interval); - if (isWin) { - spawn("taskkill", ["/pid", String(server.pid), "/T", "/F"], { stdio: "ignore" }); - } else { - server.kill("SIGTERM"); - } - process.exit(0); - } - }).on("error", () => {}); - if (attempts >= maxAttempts) { - console.error("Server did not respond with HTTP 200 within 30 seconds"); - clearInterval(interval); - if (isWin) { - spawn("taskkill", ["/pid", String(server.pid), "/T", "/F"], { stdio: "ignore" }); - } else { - server.kill("SIGTERM"); - } - process.exit(1); - } - }, 1000); + npx vite dev --port 3099 & + SERVER_PID=$! + + for i in $(seq 1 30); do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3099/ || true) + if [ "$STATUS" = "200" ]; then + echo "Server responded with HTTP 200 (attempt $i)" + kill "$SERVER_PID" 2>/dev/null || true + exit 0 + fi + sleep 1 + done + + echo "Server did not respond with HTTP 200 within 30 seconds" + kill "$SERVER_PID" 2>/dev/null || true + exit 1 e2e: name: E2E (${{ matrix.project }}) @@ -141,19 +105,23 @@ jobs: - cloudflare-workers steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup-node-pnpm - name: Build plugin run: pnpm run build + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-chromium-${{ runner.os }}-${{ hashFiles('**/package.json') }} + - run: pnpm exec playwright install chromium + - run: pnpm exec playwright test env: CI: true PLAYWRIGHT_PROJECT: ${{ matrix.project }} + - uses: actions/upload-artifact@v4 if: failure() with: diff --git a/.github/workflows/deploy-examples.yml b/.github/workflows/deploy-examples.yml index c2f610eedb..02febba46e 100644 --- a/.github/workflows/deploy-examples.yml +++ b/.github/workflows/deploy-examples.yml @@ -6,6 +6,10 @@ on: pull_request: branches: [main] +concurrency: + group: deploy-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + jobs: deploy: # Skip entirely for fork PRs — they don't have access to deploy secrets. @@ -43,15 +47,7 @@ jobs: wrangler_config: dist/server/wrangler.json steps: - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup-node-pnpm - name: Build vinext plugin run: pnpm run build @@ -116,7 +112,7 @@ jobs: script: | const prNumber = context.issue.number; const alias = `pr-${prNumber}`; - + const examples = [ { name: 'app-router-cloudflare', project: 'app-router-cloudflare' }, { name: 'pages-router-cloudflare', project: 'pages-router-cloudflare' }, @@ -126,30 +122,30 @@ jobs: { name: 'benchmarks', project: 'benchmarks' }, { name: 'hackernews', project: 'hackernews', original: 'https://next-react-server-components.vercel.app' }, ]; - + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, }); - + const marker = ''; const existingComment = comments.find(c => c.body?.includes(marker)); - + const rows = examples .map(e => { const orig = e.original ? `[original](${e.original})` : ''; return `| ${e.name} | [preview](https://${alias}-${e.project}.vinext.workers.dev) | [production](https://${e.project}.vinext.workers.dev) | ${orig} |`; }) .join('\n'); - + const body = [ marker, '| Example | Preview | Production | Original |', '|---------|---------|------------|----------|', rows, ].join('\n'); - + if (existingComment) { await github.rest.issues.updateComment({ owner: context.repo.owner, diff --git a/.github/workflows/deploy-preview-command.yml b/.github/workflows/deploy-preview-command.yml index a09befa0cf..fbadd6a667 100644 --- a/.github/workflows/deploy-preview-command.yml +++ b/.github/workflows/deploy-preview-command.yml @@ -87,14 +87,7 @@ jobs: with: ref: ${{ needs.check.outputs.sha }} - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup-node-pnpm - name: Build vinext plugin run: pnpm run build diff --git a/.github/workflows/ecosystem-run.yml b/.github/workflows/ecosystem-run.yml index bf72495107..a9fa8e398c 100644 --- a/.github/workflows/ecosystem-run.yml +++ b/.github/workflows/ecosystem-run.yml @@ -25,27 +25,20 @@ jobs: - name: Checkout vinext uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: ./.github/actions/setup-node-pnpm with: node-version: ${{ inputs.node_version }} - cache: pnpm - - name: Build vinext - run: | - pnpm install --frozen-lockfile - pnpm run build + - name: Build vinext plugin + run: pnpm run build - name: Read vinext versions id: versions run: | - node - <<'NODE' - const fs = require('fs'); - const path = require('path'); - const pkg = require('./packages/vinext/package.json'); + node --input-type=module <<'NODE' + import { readFileSync, appendFileSync } from 'node:fs'; + + const pkg = JSON.parse(readFileSync('./packages/vinext/package.json', 'utf8')); const clean = (value) => value.replace(/^[^0-9]*/, ''); const react = clean(pkg.peerDependencies.react); const reactDom = clean(pkg.peerDependencies['react-dom']); @@ -57,18 +50,18 @@ jobs: `vite=${vite}`, `rsc=${rsc}`, ].join('\n'); - fs.appendFileSync(process.env.GITHUB_OUTPUT, `${lines}\n`); + appendFileSync(process.env.GITHUB_OUTPUT, `${lines}\n`); NODE - name: Validate repo URL env: REPO_URL: ${{ inputs.repo_url }} run: | - node - <<'NODE' - const fs = require('fs'); + node --input-type=module <<'NODE' + import { readFileSync } from 'node:fs'; const input = process.env.REPO_URL; - const allowed = JSON.parse(fs.readFileSync('.github/repos.json', 'utf8')) + const allowed = JSON.parse(readFileSync('.github/repos.json', 'utf8')) .map((repo) => repo.url); if (!allowed.includes(input)) { @@ -109,42 +102,37 @@ jobs: - name: Verify build output working-directory: target-repo run: | - node - <<'NODE' - const fs = require('fs'); - const path = require('path'); + node --input-type=module <<'NODE' + import { existsSync, readdirSync } from 'node:fs'; + import { join } from 'node:path'; const ensureNonEmptyDir = (dir) => { - if (!fs.existsSync(dir)) { + if (!existsSync(dir)) { console.error(`missing ${dir}`); process.exit(1); } - const entries = fs.readdirSync(dir); + const entries = readdirSync(dir); if (entries.length === 0) { console.error(`empty ${dir}`); process.exit(1); } }; - const clientDir = 'dist/client'; - ensureNonEmptyDir(clientDir); - const hasJs = (dir) => { - const entries = fs.readdirSync(dir, { withFileTypes: true }); + const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { - const entryPath = path.join(dir, entry.name); + const entryPath = join(dir, entry.name); if (entry.isDirectory()) { - if (hasJs(entryPath)) { - return true; - } + if (hasJs(entryPath)) return true; continue; } - if (entry.name.endsWith('.js') || entry.name.endsWith('.mjs')) { - return true; - } + if (entry.name.endsWith('.js') || entry.name.endsWith('.mjs')) return true; } return false; }; + const clientDir = 'dist/client'; + ensureNonEmptyDir(clientDir); if (!hasJs(clientDir)) { console.error(`no js artifacts in ${clientDir}`); process.exit(1); @@ -152,7 +140,6 @@ jobs: const serverDir = 'dist/server'; ensureNonEmptyDir(serverDir); - if (!hasJs(serverDir)) { console.error(`no js artifacts in ${serverDir}`); process.exit(1); diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index d2f01c40d4..0d9af9ccac 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -15,11 +15,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup-node-pnpm - run: pnpm run build - run: pnpm dlx pkg-pr-new publish --pnpm './packages/vinext' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 66f58cadcb..69a5bf5f4d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,19 +36,12 @@ jobs: with: fetch-depth: 0 # full history for changelog generation - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 + - uses: ./.github/actions/setup-node-pnpm with: - node-version: 24 registry-url: https://registry.npmjs.org - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Build run: pnpm run build - working-directory: packages/vinext - name: Bump version id: version