Repository: vitejs/vite-ecosystem-ci Branch: main Commit: a0c409db48ea Files: 57 Total size: 78.7 KB Directory structure: gitextract_ju04i1kh/ ├── .editorconfig ├── .github/ │ ├── renovate.json5 │ └── workflows/ │ ├── ci.yml │ ├── ecosystem-ci-from-pr.yml │ ├── ecosystem-ci-selected.yml │ └── ecosystem-ci.yml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── builds/ │ ├── vite-plugin-react.ts │ ├── vite-plugin-svelte.ts │ └── vite-plugin-vue.ts ├── discord-webhook.ts ├── docs/ │ └── pr-comment-setup.md ├── ecosystem-ci.ts ├── eslint.config.js ├── package.json ├── pnpm-workspace.yaml ├── tests/ │ ├── _selftest.ts │ ├── analogjs.ts │ ├── astro.ts │ ├── histoire.ts │ ├── hydrogen.ts │ ├── iles.ts │ ├── ladle.ts │ ├── laravel.ts │ ├── marko.ts │ ├── nuxt.ts │ ├── nx.ts │ ├── one.ts │ ├── quasar.ts │ ├── qwik.ts │ ├── rakkas.ts │ ├── react-router.ts │ ├── redwoodjs.ts │ ├── storybook.ts │ ├── sveltekit.ts │ ├── tanstack-start.ts │ ├── unocss.ts │ ├── vike.ts │ ├── vite-environment-examples.ts │ ├── vite-plugin-cloudflare.ts │ ├── vite-plugin-laravel.ts │ ├── vite-plugin-pwa.ts │ ├── vite-plugin-react.ts │ ├── vite-plugin-rsc.ts │ ├── vite-plugin-svelte.ts │ ├── vite-plugin-vue.ts │ ├── vite-setup-catalogue.ts │ ├── vitepress.ts │ ├── vitest.ts │ ├── vuepress.ts │ └── waku.ts ├── tsconfig.json ├── types.d.ts └── utils.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf insert_final_newline = true indent_style = tab indent_size = 2 charset = utf-8 trim_trailing_whitespace = true [package.json] indent_style = space ================================================ FILE: .github/renovate.json5 ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:base", "schedule:weekly", "group:allNonMajor"], "labels": ["dependencies"], "ignorePaths": [], "rangeStrategy": "bump", "packageRules": [ { "depTypeList": ["peerDependencies", "engines"], "enabled": false, }, ], "ignoreDeps": [], } ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI env: # 7 GiB by default on GitHub, setting to 6 GiB # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources NODE_OPTIONS: --max-old-space-size=6144 # configure corepack to be strict but not download newer versions or change anything COREPACK_DEFAULT_TO_LATEST: 0 COREPACK_ENABLE_AUTO_PIN: 0 COREPACK_ENABLE_STRICT: 1 # see https://turbo.build/repo/docs/telemetry#how-do-i-opt-out TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 on: push: branches: - main pull_request: branches: - main jobs: ci: timeout-minutes: 10 runs-on: ubuntu-latest permissions: contents: read # to clone the repo steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ^24.14.1 - run: corepack enable - run: pnpm --version - uses: actions/setup-node@v6 with: node-version: ^24.14.1 cache: "pnpm" - name: install run: pnpm install --frozen-lockfile --prefer-offline - name: format run: pnpm format - name: lint run: pnpm run lint - name: typecheck run: pnpm run typecheck - name: audit if: (${{ success() }} || ${{ failure() }}) run: pnpm audit --prod --audit-level moderate - name: test if: (${{ success() }} || ${{ failure() }}) run: pnpm test:self ================================================ FILE: .github/workflows/ecosystem-ci-from-pr.yml ================================================ # integration tests for vite ecosystem - run from pr comments name: vite-ecosystem-ci-from-pr env: # 7 GiB by default on GitHub, setting to 6 GiB # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources NODE_OPTIONS: --max-old-space-size=6144 # configure corepack to be strict but not download newer versions or change anything COREPACK_DEFAULT_TO_LATEST: 0 COREPACK_ENABLE_AUTO_PIN: 0 COREPACK_ENABLE_STRICT: 1 # see https://turbo.build/repo/docs/telemetry#how-do-i-opt-out TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 on: workflow_dispatch: inputs: prNumber: description: "PR number (e.g. 9887)" required: true type: string branchName: description: "vite branch to use" required: true type: string default: "main" repo: description: "vite repository to use" required: true type: string default: "vitejs/vite" commit: description: "vite commit sha to use" type: string suite: description: "testsuite to run. runs all testsuits when `-`." required: false type: choice options: - "-" - analogjs - astro # - histoire # disabled temporarily - hydrogen - iles # - ladle # disabled until its CI is fixed - laravel - marko - nuxt - nx - one - quasar - qwik # - rakkas # disabled temporarily - react-router # - redwoodjs # disabled temporarily - storybook - sveltekit - tanstack-start - unocss - vike - vite-environment-examples - vite-plugin-pwa - vite-plugin-react - vite-plugin-svelte - vite-plugin-vue - vite-plugin-cloudflare - vite-plugin-rsc - vite-setup-catalogue - vitepress - vitest - vuepress - waku jobs: init: runs-on: ubuntu-latest outputs: comment-id: ${{ steps.create-comment.outputs.result }} permissions: {} steps: - id: generate-token uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.PR_GITHUB_APP_ID }} private-key: ${{ secrets.PR_GITHUB_APP_PRIVATE_KEY }} repositories: vite - id: create-comment uses: actions/github-script@v8 with: github-token: ${{ steps.generate-token.outputs.token }} result-encoding: string script: | const url = `${context.serverUrl}//${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` const urlLink = `[Open](${url})` const { data: comment } = await github.rest.issues.createComment({ issue_number: context.payload.inputs.prNumber, owner: context.repo.owner, repo: 'vite', body: `⏳ Triggered ecosystem CI: ${urlLink}` }) return comment.id execute-selected-suite: timeout-minutes: 30 runs-on: ubuntu-latest needs: init if: "inputs.suite != '-'" outputs: ref: ${{ steps.get-ref.outputs.ref }} permissions: contents: read # to clone the repo steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ^24.14.1 - run: corepack enable - run: pnpm --version - run: pnpm i --frozen-lockfile - run: >- node ecosystem-ci.ts --branch "$BRANCH_NAME" --repo "$REPO" $(if [ -n "$COMMIT" ] ; then echo "--commit $COMMIT"; fi) "$SUITE" env: BRANCH_NAME: ${{ inputs.branchName }} REPO: ${{ inputs.repo }} COMMIT: ${{ inputs.commit }} SUITE: ${{ inputs.suite }} - id: get-ref if: always() run: | REF="${REF:-"$(git log -1 --pretty=format:%H)"}" echo "ref=$REF" >> $GITHUB_OUTPUT env: COMMIT: ${{ inputs.commit }} working-directory: ${{ inputs.commit && '.' || 'workspace/vite' }} execute-all: timeout-minutes: 30 runs-on: ubuntu-latest needs: init if: "inputs.suite == '-'" outputs: ref: ${{ steps.get-ref.outputs.ref }} permissions: contents: read # to clone the repo strategy: matrix: suite: - analogjs - astro # - histoire # disabled temporarily # - hydrogen # disabled until they complete they migration back to Vite # - iles # disabled until its CI is fixed # - ladle # disabled until its CI is fixed - laravel - marko - nuxt # - nx # disabled temporarily # - one # disabled until we figured out how to support bun - quasar - qwik # - rakkas # disabled temporarily - react-router # - redwoodjs # disabled temporarily - storybook - sveltekit - tanstack-start - unocss - vike - vite-environment-examples - vite-plugin-pwa - vite-plugin-react - vite-plugin-svelte - vite-plugin-vue - vite-plugin-cloudflare - vite-plugin-rsc - vite-setup-catalogue - vitepress - vitest - vuepress - waku fail-fast: false steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ^24.14.1 - run: corepack enable - run: pnpm --version - run: pnpm i --frozen-lockfile - run: >- node ecosystem-ci.ts --branch "$BRANCH_NAME" --repo "$REPO" $(if [ -n "$COMMIT" ] ; then echo "--commit $COMMIT"; fi) "$SUITE" env: BRANCH_NAME: ${{ inputs.branchName }} REPO: ${{ inputs.repo }} COMMIT: ${{ inputs.commit }} SUITE: ${{ matrix.suite }} - id: get-ref if: always() run: | REF="${REF:-"$(git log -1 --pretty=format:%H)"}" echo "ref=$REF" >> $GITHUB_OUTPUT env: COMMIT: ${{ inputs.commit }} working-directory: ${{ inputs.commit && '.' || 'workspace/vite' }} update-comment: runs-on: ubuntu-latest needs: [init, execute-selected-suite, execute-all] if: always() permissions: {} steps: - id: generate-token uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.PR_GITHUB_APP_ID }} private-key: ${{ secrets.PR_GITHUB_APP_PRIVATE_KEY }} repositories: | vite vite-ecosystem-ci - uses: actions/github-script@v8 with: github-token: ${{ steps.generate-token.outputs.token }} script: | const mainRepoName = 'vite' const ref = process.env.REF const refLink = `[\`${ref.slice(0, 7)}\`](${context.serverUrl}/${context.repo.owner}/${mainRepoName}/pull/${context.payload.inputs.prNumber}/commits/${ref})` const { data: { jobs } } = await github.rest.actions.listJobsForWorkflowRun({ owner: context.repo.owner, repo: context.repo.repo, run_id: context.runId, per_page: 100 }); const selectedSuite = context.payload.inputs.suite let results if (selectedSuite !== "-") { const { conclusion, html_url } = jobs.find(job => job.name === "execute-selected-suite") results = [{ suite: selectedSuite, conclusion, link: html_url }] } else { results = jobs .filter(job => job.name.startsWith('execute-all ')) .map(job => { const suite = job.name.replace(/^execute-all \(([^)]+)\)$/, "$1") return { suite, conclusion: job.conclusion, link: job.html_url } }) } const url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` const urlLink = `[Open](${url})` const conclusionEmoji = { success: ":white_check_mark:", failure: ":x:", cancelled: ":stop_button:" } // check for previous ecosystem-ci runs against the main branch // first, list workflow runs for ecosystem-ci.yml const { data: { workflow_runs } } = await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, workflow_id: 'ecosystem-ci.yml' }); // for simplity, we only take the latest completed scheduled run // otherwise we would have to check the inputs for every maunally triggerred runs, which is an overkill const latestScheduledRun = workflow_runs.find(run => run.event === "schedule" && run.status === "completed") // get the jobs for the latest scheduled run const { data: { jobs: scheduledJobs } } = await github.rest.actions.listJobsForWorkflowRun({ owner: context.repo.owner, repo: context.repo.repo, run_id: latestScheduledRun.id }); const scheduledResults = scheduledJobs .filter(job => job.name.startsWith('test-ecosystem ')) .map(job => { const suite = job.name.replace(/^test-ecosystem \(([^)]+)\)$/, "$1") return { suite, conclusion: job.conclusion, link: job.html_url } }) const rows = [] const successfulSuitesWithoutChanges = [] results.forEach(current => { const latest = scheduledResults.find(s => s.suite === current.suite) || {} // in case a new suite is added after latest scheduled if (current.conclusion === "success" && latest.conclusion === "success") { successfulSuitesWithoutChanges.push(`[${current.suite}](${current.link})`) } else { const firstColumn = current.suite const secondColumn = `${conclusionEmoji[current.conclusion]} [${current.conclusion}](${current.link})` const thirdColumn = `${conclusionEmoji[latest.conclusion]} [${latest.conclusion}](${latest.link})` rows.push(`| ${firstColumn} | ${secondColumn} | ${thirdColumn} |`) } }) let body = ` 📝 Ran ecosystem CI on ${refLink}: ${urlLink} ` if (rows.length > 0) { body += `| suite | result | [latest scheduled](${latestScheduledRun.html_url}) | |-------|--------|----------------| ${rows.join("\n")} ${conclusionEmoji.success} ${successfulSuitesWithoutChanges.join(", ")} ` } else { body += `${conclusionEmoji.success} ${successfulSuitesWithoutChanges.join(", ")} ` } await github.rest.issues.createComment({ issue_number: context.payload.inputs.prNumber, owner: context.repo.owner, repo: mainRepoName, body }) await github.rest.issues.deleteComment({ owner: context.repo.owner, repo: mainRepoName, comment_id: +process.env.COMMENT_ID }) env: REF: ${{ needs.execute-all.outputs.ref || needs.execute-selected-suite.outputs.ref }} COMMENT_ID: ${{ needs.init.outputs.comment-id }} ================================================ FILE: .github/workflows/ecosystem-ci-selected.yml ================================================ # integration tests for vite ecosystem - single run of selected testsuite name: vite-ecosystem-ci-selected env: # 7 GiB by default on GitHub, setting to 6 GiB # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources NODE_OPTIONS: --max-old-space-size=6144 # configure corepack to be strict but not download newer versions or change anything COREPACK_DEFAULT_TO_LATEST: 0 COREPACK_ENABLE_AUTO_PIN: 0 COREPACK_ENABLE_STRICT: 1 # see https://turbo.build/repo/docs/telemetry#how-do-i-opt-out TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 on: workflow_dispatch: inputs: refType: description: "type of vite ref to use" required: true type: choice options: - branch - tag - commit - release default: "branch" ref: description: "vite ref to use" required: true type: string default: "main" repo: description: "vite repository to use" required: true type: string default: "vitejs/vite" vite_plugin_react_ref: description: "vite-plugin-react ref to use" type: string vite_plugin_react_repo: description: "vite-plugin-react repository to use" type: string rolldownRef: description: "rolldown commit sha to use from pkg.pr.new" type: string suite: description: "testsuite to run" required: true type: choice options: - analogjs - astro - histoire - hydrogen - iles - ladle - laravel - marko - nuxt - nx # - one # disabled until we figured out how to support bun - quasar - qwik - rakkas - react-router - redwoodjs - storybook - sveltekit - tanstack-start - unocss - vike - vite-environment-examples - vite-plugin-pwa - vite-plugin-react - vite-plugin-svelte - vite-plugin-vue - vite-plugin-cloudflare - vite-plugin-rsc - vite-setup-catalogue - vitepress - vitest - vuepress - waku sendDiscordReport: description: "send results to discord" type: boolean jobs: execute-selected-suite: timeout-minutes: 30 runs-on: ubuntu-latest permissions: contents: read # to clone the repo steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ^24.14.1 id: setup-node - run: corepack enable - run: pnpm --version - run: pnpm i --frozen-lockfile - run: >- node ecosystem-ci.ts "--$REF_TYPE" "$REF" --repo "$REPO" ${ROLLDOWN_REF:+--rolldown-ref "$ROLLDOWN_REF"} "$SUITE" id: ecosystem-ci-run env: REF_TYPE: ${{ inputs.refType }} REF: ${{ inputs.ref }} REPO: ${{ inputs.repo }} ROLLDOWN_REF: ${{ inputs.rolldownRef }} SUITE: ${{ inputs.suite }} VITE_PLUGIN_REACT_REF: ${{ inputs.vite_plugin_react_ref }} VITE_PLUGIN_REACT_REPO: ${{ inputs.vite_plugin_react_repo }} - if: always() && (inputs.sendDiscordReport || github.event_name != 'workflow_dispatch') run: node discord-webhook.ts env: WORKFLOW_NAME: ci-selected REF_TYPE: ${{ inputs.refType }} REF: ${{ inputs.ref }} REPO: ${{ inputs.repo }} SUITE: ${{ inputs.suite }} STATUS: ${{ job.status }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/ecosystem-ci.yml ================================================ # integration tests for vite ecosystem projects - scheduled or manual run for all suites name: vite-ecosystem-ci env: # 7 GiB by default on GitHub, setting to 6 GiB # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources NODE_OPTIONS: --max-old-space-size=6144 # configure corepack to be strict but not download newer versions or change anything COREPACK_DEFAULT_TO_LATEST: 0 COREPACK_ENABLE_AUTO_PIN: 0 COREPACK_ENABLE_STRICT: 1 # see https://turbo.build/repo/docs/telemetry#how-do-i-opt-out TURBO_TELEMETRY_DISABLED: 1 DO_NOT_TRACK: 1 on: schedule: - cron: "0 5 * * 1-5" # Monday-Friday 5AM workflow_dispatch: inputs: refType: description: "type of ref" required: true type: choice options: - branch - tag - commit - release default: "branch" ref: description: "vite ref to use" required: true type: string default: "main" repo: description: "vite repository to use" required: true type: string default: "vitejs/vite" rolldownRef: description: "rolldown commit sha to use from pkg.pr.new" type: string sendDiscordReport: description: "send results to discord" type: boolean repository_dispatch: types: [ecosystem-ci] jobs: test-ecosystem: timeout-minutes: 30 runs-on: ubuntu-latest strategy: matrix: suite: - analogjs - astro # - histoire # disabled temporarily # - hydrogen # disabled until they complete they migration back to Vite # - iles # disabled until its CI is fixed # - ladle # disabled until its CI is fixed - laravel - marko - nuxt # - one # disabled until we figured out how to support bun # - nx # disabled temporarily - quasar - qwik # - rakkas # disabled temporarily - react-router # - redwoodjs # disabled temporarily - storybook - sveltekit - tanstack-start - unocss - vike - vite-environment-examples - vite-plugin-pwa - vite-plugin-react - vite-plugin-svelte - vite-plugin-vue - vite-plugin-cloudflare - vite-plugin-rsc - vite-setup-catalogue - vitepress - vitest - vuepress - waku fail-fast: false permissions: contents: read # to clone the repo steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ^24.14.1 id: setup-node - run: corepack enable - run: pnpm --version - run: pnpm i --frozen-lockfile - run: >- node ecosystem-ci.ts "--$REF_TYPE" "$REF" --repo "$REPO" ${ROLLDOWN_REF:+--rolldown-ref "$ROLLDOWN_REF"} "$SUITE" id: ecosystem-ci-run env: REF_TYPE: ${{ inputs.refType || github.event.client_payload.refType || 'branch' }} REF: ${{ inputs.ref || github.event.client_payload.ref || 'main' }} REPO: ${{ inputs.repo || github.event.client_payload.repo || 'vitejs/vite' }} ROLLDOWN_REF: ${{ inputs.rolldownRef || github.event.client_payload.rolldownRef }} SUITE: ${{ matrix.suite }} - if: always() && (inputs.sendDiscordReport || github.event_name != 'workflow_dispatch') run: node discord-webhook.ts env: WORKFLOW_NAME: ci REF_TYPE: ${{ inputs.refType || github.event.client_payload.refType || 'branch' }} REF: ${{ inputs.ref || github.event.client_payload.ref || 'main' }} REPO: ${{ inputs.repo || github.event.client_payload.repo || 'vitejs/vite' }} SUITE: ${{ matrix.suite }} STATUS: ${{ job.status }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} IS_ROLLDOWN_VITE: "1" ================================================ FILE: .gitignore ================================================ .DS_Store .DS_Store? node_modules vite workspace .pnpm-debug.log .idea .eslintcache ================================================ FILE: .prettierrc.json ================================================ { "useTabs": true, "semi": false, "tabWidth": 2, "singleQuote": true, "printWidth": 80, "trailingComma": "all", "overrides": [ { "files": ["*.json5"], "options": { "singleQuote": false, "quoteProps": "preserve" } }, { "files": ["*.yml"], "options": { "singleQuote": false } }, { "files": "**/pnpm-lock.yaml", "options": { "requirePragma": true } }, { "files": "**/package.json", "options": { "useTabs": false, "tabWidth": 2 } } ] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021-present, Vite contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # vite-ecosystem-ci This repository is used to run integration tests for vite ecosystem projects ## via github workflow ### scheduled Workflows are scheduled to run automatically every Monday, Wednesday and Friday ### manually - open [workflow](../../actions/workflows/ecosystem-ci-selected.yml) - click 'Run workflow' button on top right of the list - select suite to run in dropdown - start workflow ## via shell script - clone this repo - run `pnpm i` - run `pnpm test` to run all suites - or `pnpm test ` to select a suite You can pass `--tag v2.8.0-beta.1`, `--branch somebranch` or `--commit abcd1234` option to select a specific vite version to build. If you pass `--release 2.7.13`, vite build will be skipped and vite is fetched from the registry instead The repositories are checked out into `workspace` subdirectory as shallow clones ## via comment on PR - comment `/ecosystem-ci run` on a PR - or `/ecosystem-ci run ` to select a suite Users with triage permission to vitejs/vite repository can only use this. See [docs/pr-comment-setup.md](./docs/pr-comment-setup.md) for how to setup this feature. # how to add a new integration test - check out the existing [tests](./tests) and add one yourself. Thanks to some utilities it is really easy - once you are confident the suite works, add it to the lists of suites in the [workflows](../../actions/) # reporting results ## Discord Results are posted automatically to `#ecosystem-ci` on [vite discord](https://chat.vitejs.dev/) ### on your own server - Go to `Server settings > Integrations > Webhooks` and click `New Webhook` - Give it a name, icon and a channel to post to - copy the webhook url - get in touch with admins of this repo so they can add the webhook #### how to add a discord webhook here - Go to `/settings/secrets/actions` and click on `New repository secret` - set `Name` as `DISCORD_WEBHOOK_URL` - paste the discord webhook url you copied from above into `Value` - Click `Add secret` ================================================ FILE: builds/vite-plugin-react.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function build(options: RunOptions) { return runInRepo({ ...options, repo: process.env.VITE_PLUGIN_REACT_REPO || 'vitejs/vite-plugin-react', branch: process.env.VITE_PLUGIN_REACT_REF || 'main', build: 'build', }) } export const packages = { '@vitejs/plugin-react': 'packages/plugin-react', '@vitejs/plugin-react-swc': 'packages/plugin-react-swc/dist', '@vitejs/plugin-rsc': 'packages/plugin-rsc', } ================================================ FILE: builds/vite-plugin-svelte.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function build(options: RunOptions) { return runInRepo({ ...options, repo: 'sveltejs/vite-plugin-svelte', branch: options.viteMajor < 8 ? 'v6' : 'main', overrides: { svelte: 'latest', }, }) } export const packages = { '@sveltejs/vite-plugin-svelte': 'packages/vite-plugin-svelte', '@sveltejs/vite-plugin-svelte-inspector': 'packages/vite-plugin-svelte-inspector', } ================================================ FILE: builds/vite-plugin-vue.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function build(options: RunOptions) { return runInRepo({ ...options, repo: 'vitejs/vite-plugin-vue', build: 'build', }) } export const packages = { '@vitejs/plugin-vue': 'packages/plugin-vue', '@vitejs/plugin-vue-jsx': 'packages/plugin-vue-jsx', } ================================================ FILE: discord-webhook.ts ================================================ import { getPermanentRef, setupEnvironment } from './utils.ts' type RefType = 'branch' | 'tag' | 'commit' | 'release' type Status = 'success' | 'failure' | 'cancelled' type Env = { WORKFLOW_NAME?: string REF_TYPE?: RefType REF?: string REPO?: string SUITE?: string STATUS?: Status DISCORD_WEBHOOK_URL?: string IS_ROLLDOWN_VITE?: '1' } const statusConfig = { success: { color: parseInt('57ab5a', 16), emoji: ':white_check_mark:', }, expectedFailure: { color: parseInt('c69026', 16), emoji: ':construction:', }, failure: { color: parseInt('e5534b', 16), emoji: ':x:', }, cancelled: { color: parseInt('768390', 16), emoji: ':stop_button:', }, } async function run() { if (!process.env.GITHUB_ACTIONS) { throw new Error('This script can only run on GitHub Actions.') } if (!process.env.DISCORD_WEBHOOK_URL) { console.warn( "Skipped beacuse process.env.DISCORD_WEBHOOK_URL was empty or didn't exist", ) return } if (!process.env.GITHUB_TOKEN) { console.warn( "Not using a token because process.env.GITHUB_TOKEN was empty or didn't exist", ) } const env = process.env as Env assertEnv('WORKFLOW_NAME', env.WORKFLOW_NAME) assertEnv('REF_TYPE', env.REF_TYPE) assertEnv('REF', env.REF) assertEnv('REPO', env.REPO) assertEnv('SUITE', env.SUITE) assertEnv('STATUS', env.STATUS) assertEnv('DISCORD_WEBHOOK_URL', env.DISCORD_WEBHOOK_URL) const isRolldownVite = !!env.IS_ROLLDOWN_VITE const expectedFailureReason = isRolldownVite ? await loadExpectedFailureReason(env.SUITE) : undefined const status = env.STATUS === 'failure' && expectedFailureReason ? 'expectedFailure' : env.STATUS await setupEnvironment() const refType = env.REF_TYPE // vite repo is not cloned when release const permRef = refType === 'release' ? undefined : await getPermanentRef() const targetText = createTargetText(refType, env.REF, permRef, env.REPO) const webhookContent = { username: `vite-ecosystem-ci (${env.WORKFLOW_NAME})`, avatar_url: 'https://github.com/vitejs.png', embeds: [ { title: `${statusConfig[status].emoji} ${env.SUITE}`, description: await createDescription( env.SUITE, targetText, expectedFailureReason, ), color: statusConfig[status].color, }, ], } const res = await fetch(env.DISCORD_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(webhookContent), }) if (res.ok) { console.log('Sent Webhook') } else { console.error(`Webhook failed ${res.status}:`, await res.text()) } } function assertEnv( name: string, value: T, ): asserts value is Exclude { if (!value) { throw new Error(`process.env.${name} is empty or does not exist.`) } } async function loadExpectedFailureReason(suite: string) { const module = await import(`./tests/${suite}.ts`) const reason: string | undefined = module.rolldownViteExpectedFailureReason return reason?.trim() } async function createRunUrl(suite: string) { const result = await fetchJobs() if (!result) { return undefined } if (result.total_count <= 0) { console.warn('total_count was 0') return undefined } const job = result.jobs.find((job) => job.name === process.env.GITHUB_JOB) if (job) { return job.html_url } // when matrix const jobM = result.jobs.find( (job) => job.name === `${process.env.GITHUB_JOB} (${suite})`, ) return jobM?.html_url } interface GitHubActionsJob { name: string html_url: string } async function fetchJobs() { const url = `${process.env.GITHUB_API_URL}/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}/jobs` const res = await fetch(url, { headers: { Accept: 'application/vnd.github.v3+json', ...(process.env.GITHUB_TOKEN ? { Authorization: `token ${process.env.GITHUB_TOKEN}`, } : undefined), }, }) if (!res.ok) { console.warn( `Failed to fetch jobs (${res.status} ${res.statusText}): ${res.text()}`, ) return null } const result = await res.json() return result as { total_count: number jobs: GitHubActionsJob[] } } async function createDescription( suite: string, targetText: string, expectedFailureReason: string | undefined, ) { const runUrl = await createRunUrl(suite) const open = runUrl === undefined ? 'Null' : `[Open](${runUrl})` let message = ` :scroll:\u00a0\u00a0${open}\u3000\u3000:zap:\u00a0\u00a0${targetText} `.trim() if (expectedFailureReason) { message += '\n' + ` :bulb:\u00a0\u00a0${expectedFailureReason} `.trim() } return message } function createTargetText( refType: RefType, ref: string, permRef: string | undefined, repo: string, ) { const repoText = repo !== 'vitejs/vite' ? `${repo}:` : '' if (refType === 'branch') { const shortRef = permRef?.slice(0, 7) const link = `https://github.com/${repo}/commits/${permRef || ref}` return `[${repoText}${ref} (${shortRef || 'unknown'})](${link})` } const refTypeText = refType === 'release' ? ' (release)' : '' const link = `https://github.com/${repo}/commits/${ref}` return `[${repoText}${ref}${refTypeText}](${link})` } run().catch((e) => { console.error('Error sending webhook:', e) }) ================================================ FILE: docs/pr-comment-setup.md ================================================ # Setting up "PR comment trigger" feature ## (1) Create a GitHub App 1. [Create a GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app). Webhook is not needed. The following permissions are required: - Metadata: Read only - Actions: Read and Write - Issues: Read and Write - Pull requests: Read and Write 1. Install that App to the organization/user. Give that App access to vitejs/vite and vitejs/vite-ecosystem-ci. 1. Check the App ID. It's written on `https://github.com/settings/apps/`. This is used later. ![GitHub App ID](github_app_id.png) 1. Generate a private key. It can be generated on the same page with the App ID. The key will be downloaded when you generate it. ![GitHub App private key](github_app_private_key.png) ## (2) Adding secrets to vitejs/vite and vitejs/vite-ecosystem-ci - vitejs/vite - `ECOSYSTEM_CI_GITHUB_APP_ID`: ID of the created GitHub App - `ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY`: the content of the private key of the created GitHub App - vitejs/vite-ecosystem-ci - `PR_GITHUB_APP_ID`: ID of the created GitHub App - `PR_GITHUB_APP_PRIVATE_KEY`: the content of the private key of the created GitHub App ## (3) Adding workflows to vitejs/vite Add [this workflow](https://github.com/vitejs/vite/blob/main/.github/workflows/ecosystem-ci-trigger.yml). ================================================ FILE: ecosystem-ci.ts ================================================ import fs from 'fs' import path from 'path' import process from 'process' import { cac } from 'cac' import { setupEnvironment, setupViteRepo, buildVite, bisectVite, parseViteMajor, parseMajorVersion, } from './utils.ts' import type { CommandOptions, RunOptions } from './types.d.ts' const cli = cac() cli .command('[...suites]', 'build vite and run selected suites') .option('--verify', 'verify checkouts by running tests', { default: false }) .option('--repo ', 'vite repository to use', { default: 'vitejs/vite' }) .option('--branch ', 'vite branch to use', { default: 'main' }) .option('--tag ', 'vite tag to use') .option('--commit ', 'vite commit sha to use') .option('--release ', 'vite release to use from npm registry') .option( '--rolldown-ref ', 'rolldown commit sha to use from pkg.pr.new', ) .action(async (suites, options: CommandOptions) => { if (options.commit) { const url = `https://pkg.pr.new/vite@${options.commit}` const { status } = await fetch(url) if (status === 200) { options.release = url delete options.commit console.log(`continuous release available on ${url}`) } } let rolldownRelease: string | undefined if (options.rolldownRef) { const url = `https://pkg.pr.new/rolldown@${options.rolldownRef}` const { status } = await fetch(url) if (status === 200) { rolldownRelease = url console.log(`rolldown continuous release available on ${url}`) } else { throw new Error( `rolldown continuous release not found for ref ${options.rolldownRef} (HTTP ${status}): ${url}`, ) } } const { root, vitePath, workspace } = await setupEnvironment() const suitesToRun = getSuitesToRun(suites, root) let viteMajor if (!options.release) { await setupViteRepo(options) await buildVite({ verify: options.verify, rolldownRelease }) viteMajor = parseViteMajor(vitePath) } else { viteMajor = parseMajorVersion(options.release) } const runOptions: RunOptions = { root, vitePath, viteMajor, workspace, release: options.release, rolldownRelease, verify: options.verify, skipGit: false, } for (const suite of suitesToRun) { await run(suite, runOptions) } }) cli .command('build-vite', 'build vite only') .option('--verify', 'verify vite checkout by running tests', { default: false, }) .option('--repo ', 'vite repository to use', { default: 'vitejs/vite' }) .option('--branch ', 'vite branch to use', { default: 'main' }) .option('--tag ', 'vite tag to use') .option('--commit ', 'vite commit sha to use') .action(async (options: CommandOptions) => { await setupEnvironment() await setupViteRepo(options) await buildVite({ verify: options.verify }) }) cli .command('run-suites [...suites]', 'run single suite with pre-built vite') .option( '--verify', 'verify checkout by running tests before using local vite', { default: false }, ) .option('--repo ', 'vite repository to use', { default: 'vitejs/vite' }) .option('--release ', 'vite release to use from npm registry') .action(async (suites, options: CommandOptions) => { const { root, vitePath, workspace } = await setupEnvironment() const suitesToRun = getSuitesToRun(suites, root) const runOptions: RunOptions = { ...options, root, vitePath, viteMajor: parseViteMajor(vitePath), workspace, } for (const suite of suitesToRun) { await run(suite, runOptions) } }) cli .command( 'bisect [...suites]', 'use git bisect to find a commit in vite that broke suites', ) .option('--good ', 'last known good ref, e.g. a previous tag. REQUIRED!') .option('--verify', 'verify checkouts by running tests', { default: false }) .option('--repo ', 'vite repository to use', { default: 'vitejs/vite' }) .option('--branch ', 'vite branch to use', { default: 'main' }) .option('--tag ', 'vite tag to use') .option('--commit ', 'vite commit sha to use') .action(async (suites, options: CommandOptions & { good: string }) => { if (!options.good) { console.log( 'you have to specify a known good version with `--good `', ) process.exit(1) } const { root, vitePath, workspace } = await setupEnvironment() const suitesToRun = getSuitesToRun(suites, root) let isFirstRun = true const { verify } = options const runSuite = async () => { try { await buildVite({ verify: isFirstRun && verify }) for (const suite of suitesToRun) { await run(suite, { verify: !!(isFirstRun && verify), skipGit: !isFirstRun, root, vitePath, viteMajor: parseViteMajor(vitePath), workspace, }) } isFirstRun = false return null } catch (e) { return e } } await setupViteRepo({ ...options, shallow: false }) const initialError = await runSuite() if (initialError) { await bisectVite(options.good, runSuite) } else { console.log(`no errors for starting commit, cannot bisect`) } }) cli.help() cli.parse() async function run(suite: string, options: RunOptions) { const { test } = await import(`./tests/${suite}.ts`) await test({ ...options, workspace: path.resolve(options.workspace, suite), }) } function getSuitesToRun(suites: string[], root: string) { let suitesToRun: string[] = suites const availableSuites: string[] = fs .readdirSync(path.join(root, 'tests')) .filter((f: string) => !f.startsWith('_') && f.endsWith('.ts')) .map((f: string) => f.slice(0, -3)) availableSuites.sort() if (suitesToRun.length === 0) { suitesToRun = availableSuites } else { const invalidSuites = suitesToRun.filter( (x) => !x.startsWith('_') && !availableSuites.includes(x), ) if (invalidSuites.length) { console.log(`invalid suite(s): ${invalidSuites.join(', ')}`) console.log(`available suites: ${availableSuites.join(', ')}`) process.exit(1) } } return suitesToRun } ================================================ FILE: eslint.config.js ================================================ // @ts-check import eslint from '@eslint/js' import n from 'eslint-plugin-n' import tseslint from 'typescript-eslint' import prettierConfig from 'eslint-config-prettier/flat' export default tseslint.config([ { name: 'local/ignores', ignores: ['workspace/**'], }, eslint.configs.recommended, tseslint.configs.recommended, n.configs['flat/recommended-module'], prettierConfig, { name: 'local/rules', files: ['**/*.{js,ts}'], rules: { eqeqeq: ['warn', 'always', { null: 'never' }], 'no-debugger': ['error'], 'no-empty': ['warn', { allowEmptyCatch: true }], 'no-process-exit': 'off', 'no-useless-escape': 'off', 'prefer-const': [ 'warn', { destructuring: 'all', }, ], 'n/no-process-exit': 'off', '@typescript-eslint/no-explicit-any': 'off', // we use any in some places }, }, ]) ================================================ FILE: package.json ================================================ { "name": "vite-ecosystem-ci", "private": true, "version": "0.0.1", "description": "Vite Ecosystem CI", "scripts": { "prepare": "pnpm exec simple-git-hooks", "lint": "eslint --cache '**/*.{js,ts}'", "lint:fix": "pnpm lint --fix", "typecheck": "tsc", "format": "prettier --ignore-path .gitignore --check .", "format:fix": "pnpm format --write", "test:self": "node ecosystem-ci.ts _selftest", "test": "node ecosystem-ci.ts", "bisect": "node ecosystem-ci.ts bisect" }, "simple-git-hooks": { "pre-commit": "pnpm exec lint-staged --concurrent false" }, "lint-staged": { "*": [ "prettier --write --ignore-unknown" ], "*.{js,ts}": [ "eslint --fix" ] }, "packageManager": "pnpm@10.32.1", "type": "module", "engines": { "node": ">=24", "pnpm": "^10.0.0" }, "repository": { "type": "git", "url": "git+https://github.com/vitejs/vite-ecosystem-ci.git" }, "license": "MIT", "bugs": { "url": "https://github.com/vitejs/vite-ecosystem-ci/issues" }, "homepage": "https://github.com/vitejs/vite-ecosystem-ci#readme", "dependencies": { "@actions/core": "^3.0.0", "cac": "^7.0.0", "execa": "^9.6.1" }, "devDependencies": { "@antfu/ni": "^30.0.0", "@eslint/js": "^10.0.1", "@types/node": "^25.5.0", "@types/pacote": "^11.1.8", "@types/semver": "^7.7.1", "eslint": "^10.1.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-n": "^17.24.0", "lint-staged": "^16.4.0", "pacote": "^21.5.0", "prettier": "^3.8.1", "semver": "^7.7.4", "simple-git-hooks": "^2.13.1", "typescript": "^5.9.3", "typescript-eslint": "^8.57.2" } } ================================================ FILE: pnpm-workspace.yaml ================================================ engineStrict: true strictPeerDependencies: false packageManagerStrict: false onlyBuiltDependencies: - esbuild - simple-git-hooks overrides: 'cross-spawn@>=7.0.0 <7.0.5': '^7.0.6' ================================================ FILE: tests/_selftest.ts ================================================ import path from 'path' import fs from 'fs' import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'vitejs/vite-ecosystem-ci', build: async () => { const dir = path.resolve(options.workspace, 'vite-ecosystem-ci') const pkgFile = path.join(dir, 'package.json') const pkg = JSON.parse(await fs.promises.readFile(pkgFile, 'utf-8')) if (pkg.name !== 'vite-ecosystem-ci') { throw new Error( `invalid checkout, expected package.json with "name":"vite-ecosystem-ci" in ${dir}`, ) } pkg.scripts.selftestscript = "[ -d ../../vite/packages/vite/dist ] || (echo 'vite build failed' && exit 1)" await fs.promises.writeFile( pkgFile, JSON.stringify(pkg, null, 2), 'utf-8', ) }, test: 'pnpm run selftestscript', verify: false, }) } ================================================ FILE: tests/analogjs.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'analogjs/analog', branch: 'beta', build: 'build:vite-ci', beforeTest: 'pnpm playwright install chromium', test: 'test:vite-ci', }) } ================================================ FILE: tests/astro.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'withastro/astro', branch: 'main', build: 'build:ci', test: 'test:vite-ci', }) } ================================================ FILE: tests/histoire.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'histoire-dev/histoire', branch: 'main', build: 'build', test: ['test', 'test:examples'], }) } export const rolldownViteExpectedFailureReason = ` needs to be updated on histoire side (manualChunks) ` ================================================ FILE: tests/hydrogen.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'Shopify/hydrogen', build: 'build', test: 'test:vite-ci', }) } ================================================ FILE: tests/iles.ts ================================================ import { runInRepo, $ } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'ElMassimo/iles', overrides: { '@vitejs/plugin-vue': true, }, beforeInstall: async () => $`git lfs install && git lfs pull`, build: 'build:all', test: 'test', }) } ================================================ FILE: tests/ladle.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'tajo/ladle', branch: 'main', build: 'build', beforeTest: 'pnpm playwright install chromium', test: 'test', }) } ================================================ FILE: tests/laravel.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' import path from 'node:path' import fs from 'node:fs' export async function test(options: RunOptions) { //see https://github.com/laravel/vite-plugin/blob/73466441b0c9eb0c1a5ce0a0e937bd83eaef4b70/.github/workflows/tests.yml#L10 process.env.LARAVEL_BYPASS_ENV_CHECK = '1' await runInRepo({ ...options, repo: 'laravel/vite-plugin', branch: '2.x', build: 'build', async beforeTest() { // Add `vitest.config.ts` to exclude Vite from inlined by Vitest. // Otherwise the mock here doesn't work. // https://github.com/laravel/vite-plugin/blob/3f7bf9eddc69580796c26890c99065d7259c785e/tests/index.test.ts#L7-L22 const dir = path.resolve(options.workspace, 'vite-plugin') const vitestConfigFile = path.join(dir, 'vitest.config.ts') fs.writeFileSync( vitestConfigFile, getVitestConfig(options.vitePath), 'utf-8', ) }, test: 'test', agent: 'npm', }) } const getVitestConfig = (viteRepoPath: string) => /* ts */ ` import { defineConfig } from "vitest/config"; export default defineConfig({ test: { deps: { moduleDirectories: [ ${JSON.stringify(path.resolve(viteRepoPath, 'packages'))}, ], }, }, }); ` ================================================ FILE: tests/marko.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'marko-js/vite', dir: 'marko', // default is last segment of repo, which would be vite and confusing build: 'build', beforeTest: 'pnpm playwright install chromium', test: 'test', overrides: { esbuild: true, }, }) } ================================================ FILE: tests/nuxt.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'nuxt/nuxt', overrides: { '@vitejs/plugin-vue': true, }, build: ['dev:prepare', 'build'], beforeTest: 'pnpm playwright-core install', test: ['test:fixtures', 'test:fixtures:dev', 'test:types'], }) } ================================================ FILE: tests/nx.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'nrwl/nx', branch: 'master', build: { script: 'build-project', args: ['vite', '--skip-nx-cache'] }, test: [ { script: 'test', args: ['vite', '--skip-nx-cache'] }, { script: 'e2e', args: ['e2e-vite', '--skip-nx-cache'] }, ], overrides: { rollup: false, }, }) } ================================================ FILE: tests/one.ts ================================================ import type { RunOptions } from '../types.d.ts' import { runInRepo } from '../utils.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'onejs/one', branch: 'main', build: ['clean:build', 'build'], beforeTest: 'yarn playwright install chromium', test: 'test:vite-ecosystem-ci', }) } export const rolldownViteExpectedFailureReason = ` needs to be updated on one side (type incompatibility) ` ================================================ FILE: tests/quasar.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'quasarframework/quasar', branch: 'dev', overrides: { '@vitejs/plugin-vue': true, }, build: 'vite-ecosystem-ci:build', test: 'vite-ecosystem-ci:test', }) } ================================================ FILE: tests/qwik.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'BuilderIO/qwik', build: 'build.vite', beforeTest: 'pnpm playwright install chromium', test: 'test.vite', }) } ================================================ FILE: tests/rakkas.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' import { execSync } from 'node:child_process' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'rakkasjs/rakkasjs', branch: 'main', build: 'build', // This is needed to run puppeteer in Ubuntu 23+ // https://github.com/puppeteer/puppeteer/pull/13196 beforeTest: [ process.env.GITHUB_ACTIONS ? async () => { execSync( 'echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns', ) } : null, 'pnpm --dir testbed/examples exec puppeteer browsers install chrome', ].filter((x) => x != null), test: 'vite-ecosystem-ci', }) } export const rolldownViteExpectedFailureReason = ` needs to be updated on rakkas side ("moduleResolution" should be "bundler" or "nodenext") ` ================================================ FILE: tests/react-router.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'remix-run/react-router', branch: 'dev', build: 'vite-ecosystem-ci:build', beforeTest: 'vite-ecosystem-ci:before-test', test: 'vite-ecosystem-ci:test', overrides: { '@vitejs/plugin-rsc': true, }, }) } export const rolldownViteExpectedFailureReason = ` needs to be updated on react-router side (incorrect tests) ` ================================================ FILE: tests/redwoodjs.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'redwoodjs/redwood', build: { script: 'build', args: ['--skip-nx-cache'] }, test: { script: 'test-ci', args: ['--skip-nx-cache'] }, }) } ================================================ FILE: tests/storybook.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'storybookjs/storybook', branch: 'next', build: 'vite-ecosystem-ci:build', beforeTest: 'vite-ecosystem-ci:before-test', test: 'vite-ecosystem-ci:test', }) } ================================================ FILE: tests/sveltekit.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'sveltejs/kit', branch: 'main', overrides: { svelte: 'latest', '@sveltejs/vite-plugin-svelte': true, '@sveltejs/vite-plugin-svelte-inspector': true, }, beforeTest: 'pnpm playwright install chromium', test: [ 'test:vite-ecosystem-ci', 'pnpm --dir packages/kit check', // only run checks for kit package, not the whole repo ], }) } ================================================ FILE: tests/tanstack-start.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'TanStack/router', branch: 'main', build: 'vite-ecosystem-ci:build', beforeTest: 'vite-ecosystem-ci:before-test', test: 'vite-ecosystem-ci:test', }) } ================================================ FILE: tests/unocss.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'unocss/unocss', build: 'build', test: 'test', }) } ================================================ FILE: tests/vike.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'vikejs/vike', branch: 'main', overrides: { '@vitejs/plugin-react': true, '@vitejs/plugin-react-swc': true, '@vitejs/plugin-vue': true, }, build: 'build', beforeTest: 'pnpm exec playwright install chromium', test: 'test:vite-ecosystem-ci', }) } ================================================ FILE: tests/vite-environment-examples.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { if (options.viteMajor < 6) { return } await runInRepo({ ...options, repo: 'hi-ogawa/vite-environment-examples', branch: 'main', build: 'vite-ecosystem-ci:build', beforeTest: 'vite-ecosystem-ci:before-test', test: 'vite-ecosystem-ci:test', }) } ================================================ FILE: tests/vite-plugin-cloudflare.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'cloudflare/workers-sdk', test: 'pnpm test:ci -F @vite-plugin-cloudflare/playground', }) } ================================================ FILE: tests/vite-plugin-laravel.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'innocenzi/laravel-vite', build: 'build', test: 'test:vite', }) } ================================================ FILE: tests/vite-plugin-pwa.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'vite-pwa/vite-plugin-pwa', branch: 'main', beforeTest: 'pnpm playwright install chromium', build: 'build', test: 'test:vite-ecosystem-ci', }) } ================================================ FILE: tests/vite-plugin-react.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'vitejs/vite-plugin-react', build: 'build', beforeTest: 'pnpm playwright install chromium', test: ['test', 'typecheck'], }) } ================================================ FILE: tests/vite-plugin-rsc.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'vitejs/vite-plugin-react', build: 'build', beforeTest: 'pnpm playwright install chromium', test: [ 'pnpm -C packages/plugin-rsc test-e2e', 'pnpm -C packages/plugin-rsc tsc', ], }) } ================================================ FILE: tests/vite-plugin-svelte.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'sveltejs/vite-plugin-svelte', branch: options.viteMajor < 8 ? 'v6' : 'main', beforeTest: 'pnpm playwright install chromium', test: ['check:lint', 'check:types', 'test'], }) } ================================================ FILE: tests/vite-plugin-vue.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'vitejs/vite-plugin-vue', build: 'build', beforeTest: 'pnpm playwright install chromium', test: 'test', }) } ================================================ FILE: tests/vite-setup-catalogue.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'sapphi-red/vite-setup-catalogue', branch: 'main', test: 'test-for-ecosystem-ci', }) } ================================================ FILE: tests/vitepress.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'vuejs/vitepress', overrides: { '@vitejs/plugin-vue': true, }, branch: 'main', build: 'build', beforeTest: 'pnpm playwright install chromium', test: 'test', }) } ================================================ FILE: tests/vitest.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'vitest-dev/vitest', build: 'build', test: ['test:ecosystem-ci', 'test:examples'], beforeTest: 'pnpm playwright install chromium', }) } ================================================ FILE: tests/vuepress.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'vuepress/core', overrides: { '@vitejs/plugin-vue': true, }, branch: 'main', build: 'build', beforeTest: 'pnpm --filter e2e exec playwright install chromium', test: 'test', }) } ================================================ FILE: tests/waku.ts ================================================ import { runInRepo } from '../utils.ts' import type { RunOptions } from '../types.d.ts' export async function test(options: RunOptions) { await runInRepo({ ...options, repo: 'dai-shi/waku', branch: 'main', build: 'compile', beforeTest: 'pnpm playwright install chromium', test: 'test-vite-ecosystem-ci', overrides: { '@vitejs/plugin-rsc': true, // It uses Vitest 3.2+ so we don't need to inject the overrides. // If we inject overrides, the following error happens due to how waku sets overrides for the test. // // npm error code EINVALIDTAGNAME // npm error Invalid tag name "<3.2.0>vite" of package "vitest@<3.2.0>vite": Tags may not have any characters that encodeURIComponent encodes. vitest: false, }, }) } ================================================ FILE: tsconfig.json ================================================ { "include": ["./**/*.ts"], "exclude": ["**/node_modules/**", "./workspace/**"], "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "bundler", "allowImportingTsExtensions": true, "noEmit": true, "skipLibCheck": true, "strict": true, "declaration": true, "noImplicitOverride": true, "noUnusedLocals": true, "esModuleInterop": true, "useUnknownInCatchVariables": false, "allowSyntheticDefaultImports": true, "lib": ["esnext"], "sourceMap": true, "erasableSyntaxOnly": true, "verbatimModuleSyntax": true } } ================================================ FILE: types.d.ts ================================================ import type { AGENTS } from '@antfu/ni' export interface EnvironmentData { root: string workspace: string vitePath: string cwd: string env: ProcessEnv } export interface RunOptions { workspace: string root: string vitePath: string viteMajor: number verify?: boolean skipGit?: boolean release?: string rolldownRelease?: string agent?: (typeof AGENTS)[number] build?: Task | Task[] test?: Task | Task[] beforeInstall?: Task | Task[] beforeBuild?: Task | Task[] beforeTest?: Task | Task[] } type Task = string | { script: string; args?: string[] } | (() => Promise) export interface CommandOptions { suites?: string[] repo?: string branch?: string tag?: string commit?: string release?: string verify?: boolean skipGit?: boolean rolldownRef?: string } export interface RepoOptions { repo: string dir?: string branch?: string tag?: string commit?: string shallow?: boolean overrides?: Overrides } export interface Overrides { [key: string]: string | boolean } export interface ProcessEnv { [key: string]: string | undefined } interface DependencyInfo { from: string version: string resolved: string path: string } interface PackageInfo { name: string version: string path: string private: boolean dependencies: Record devDependencies: Record optionalDependencies: Record } ================================================ FILE: utils.ts ================================================ import path from 'path' import fs from 'fs' import { fileURLToPath, pathToFileURL } from 'url' import { execaCommand } from 'execa' import type { PackageInfo, EnvironmentData, Overrides, ProcessEnv, RepoOptions, RunOptions, Task, } from './types.d.ts' import { detect, AGENTS, getCommand, serializeCommand } from '@antfu/ni' import * as actionsCore from '@actions/core' import * as semver from 'semver' import pacote from 'pacote' const isGitHubActions = !!process.env.GITHUB_ACTIONS let vitePath: string let cwd: string let env: ProcessEnv function cd(dir: string) { cwd = path.resolve(cwd, dir) } export async function $(literals: TemplateStringsArray, ...values: any[]) { const cmd = literals.reduce( (result, current, i) => result + current + (values?.[i] != null ? `${values[i]}` : ''), '', ) if (isGitHubActions) { actionsCore.startGroup(`${cwd} $> ${cmd}`) } else { console.log(`${cwd} $> ${cmd}`) } const proc = execaCommand(cmd, { env, stdio: 'pipe', cwd, }) if (proc.stdin) process.stdin.pipe(proc.stdin) if (proc.stdout) proc.stdout.pipe(process.stdout) if (proc.stderr) proc.stderr.pipe(process.stderr) let result try { result = await proc } catch (error) { // Since we already piped the io to the parent process, we remove the duplicated // messages here so it's easier to read the error message. if (error.stdout) error.stdout = 'value removed by vite-ecosystem-ci' if (error.stderr) error.stderr = 'value removed by vite-ecosystem-ci' if (error.stdio) error.stdio = ['value removed by vite-ecosystem-ci'] throw error } if (isGitHubActions) { actionsCore.endGroup() } return result.stdout } export async function setupEnvironment(): Promise { const root = dirnameFrom(import.meta.url) const workspace = path.resolve(root, 'workspace') vitePath = path.resolve(workspace, 'vite') cwd = process.cwd() env = { ...process.env, CI: 'true', TURBO_FORCE: 'true', // disable turbo caching, ecosystem-ci modifies things and we don't want replays YARN_ENABLE_IMMUTABLE_INSTALLS: 'false', // to avoid errors with mutated lockfile due to overrides NODE_OPTIONS: '--max-old-space-size=6144', // GITHUB CI has 7GB max, stay below ECOSYSTEM_CI: 'true', // flag for tests, can be used to conditionally skip irrelevant tests. TURBO_TELEMETRY_DISABLED: '1', // # see https://turbo.build/repo/docs/telemetry#how-do-i-opt-out DO_NOT_TRACK: '1', } initWorkspace(workspace) return { root, workspace, vitePath, cwd, env } } function initWorkspace(workspace: string) { if (!fs.existsSync(workspace)) { fs.mkdirSync(workspace, { recursive: true }) } const eslintrc = path.join(workspace, '.eslintrc.json') if (!fs.existsSync(eslintrc)) { fs.writeFileSync(eslintrc, '{"root":true}\n', 'utf-8') } const editorconfig = path.join(workspace, '.editorconfig') if (!fs.existsSync(editorconfig)) { fs.writeFileSync(editorconfig, 'root = true\n', 'utf-8') } const tsconfig = path.join(workspace, 'tsconfig.json') if (!fs.existsSync(tsconfig)) { fs.writeFileSync(tsconfig, '{}\n', 'utf-8') } } export async function setupRepo(options: RepoOptions) { if (options.branch == null) { options.branch = 'main' } if (options.shallow == null) { options.shallow = true } let { repo, commit, branch, tag, dir, shallow } = options if (!dir) { throw new Error('setupRepo must be called with options.dir') } if (!repo.includes(':')) { repo = `https://github.com/${repo}.git` } let needClone = true if (fs.existsSync(dir)) { const _cwd = cwd cd(dir) let currentClonedRepo: string | undefined try { currentClonedRepo = await $`git ls-remote --get-url` } catch { // when not a git repo } if (repo === currentClonedRepo) { const isShallow = (await $`git rev-parse --is-shallow-repository`).trim() === 'true' if (isShallow === shallow) { needClone = false } } cd(_cwd) if (needClone) { fs.rmSync(dir, { recursive: true, force: true }) } } if (needClone) { await $`git -c advice.detachedHead=false clone ${ shallow ? '--depth=1 --no-tags' : '' } --branch ${tag || branch} ${repo} ${dir}` } cd(dir) await $`git clean -fdxq` if (!needClone && shallow && !commit) { await $`git remote set-branches origin ${branch}` } await $`git fetch ${shallow ? '--depth=1 --no-tags' : '--tags'} origin ${ tag ? `tag ${tag}` : `${commit || branch}` }` if (shallow) { await $`git -c advice.detachedHead=false checkout ${ tag ? `tags/${tag}` : `${commit || branch}` }` } else { await $`git checkout ${branch}` await $`git merge FETCH_HEAD` if (tag || commit) { await $`git reset --hard ${tag || commit}` } } } function toCommand( task: Task | Task[] | void, agent: (typeof AGENTS)[number], ): ((scripts: any) => Promise) | void { return async (scripts: any) => { const tasks = Array.isArray(task) ? task : [task] for (const task of tasks) { if (task == null || task === '') { continue } else if (typeof task === 'string') { if (scripts[task] != null) { const runTaskWithAgent = getCommand(agent, 'run', [task]) await $`${serializeCommand(runTaskWithAgent)}` } else { await $`${task}` } } else if (typeof task === 'function') { await task() } else if (task?.script) { if (scripts[task.script] != null) { const runTaskWithAgent = getCommand(agent, 'run', [ task.script, ...(task.args ?? []), ]) await $`${serializeCommand(runTaskWithAgent)}` } else { throw new Error( `invalid task, script "${task.script}" does not exist in package.json`, ) } } else { throw new Error( `invalid task, expected string or function but got ${typeof task}: ${task}`, ) } } } } export async function runInRepo(options: RunOptions & RepoOptions) { if (options.verify == null) { options.verify = true } if (options.skipGit == null) { options.skipGit = false } if (options.branch == null) { options.branch = 'main' } const { build, test, repo, branch, tag, commit, skipGit, verify, beforeInstall, beforeBuild, beforeTest, } = options const dir = path.resolve( options.workspace, options.dir || repo.substring(repo.lastIndexOf('/') + 1), ) if (!skipGit) { await setupRepo({ repo, dir, branch, tag, commit }) } else { cd(dir) } if (options.agent == null) { const detectedAgent = await detect({ cwd: dir, autoInstall: false }) if (detectedAgent == null) { throw new Error(`Failed to detect packagemanager in ${dir}`) } options.agent = detectedAgent } if (!AGENTS.includes(options.agent)) { throw new Error( `Invalid agent ${options.agent}. Allowed values: ${AGENTS.join(', ')}`, ) } const agent = options.agent const beforeInstallCommand = toCommand(beforeInstall, agent) const beforeBuildCommand = toCommand(beforeBuild, agent) const beforeTestCommand = toCommand(beforeTest, agent) const buildCommand = toCommand(build, agent) const testCommand = toCommand(test, agent) const pkgFile = path.join(dir, 'package.json') const pkg = JSON.parse(await fs.promises.readFile(pkgFile, 'utf-8')) await beforeInstallCommand?.(pkg.scripts) if (verify && test) { const frozenInstall = getCommand(agent, 'frozen') await $`${serializeCommand(frozenInstall)}` await beforeBuildCommand?.(pkg.scripts) await buildCommand?.(pkg.scripts) await beforeTestCommand?.(pkg.scripts) await testCommand?.(pkg.scripts) } let overrides = options.overrides || {} if (options.release) { if (overrides.vite && overrides.vite !== options.release) { throw new Error( `conflicting overrides.vite=${overrides.vite} and --release=${options.release} config. Use either one or the other`, ) } else { overrides.vite = options.release } if ( overrides.rollup !== false || overrides.esbuild === true || overrides.vitest !== false ) { const viteManifest = await pacote.manifest(`vite@${options.release}`, { retry: { // enable retry with same options with pnpm (https://pnpm.io/settings#fetchretries) retries: 2, factor: 10, minTimeout: 10 * 1000, maxTimeout: 60 * 1000, }, }) // skip if `overrides.rollup` is `false` if (overrides.rollup !== false) { overrides.rollup = viteManifest.dependencies!.rollup } // apply if `overrides.esbuild` is `true` if (overrides.esbuild === true) { overrides.esbuild = viteManifest.dependencies!.esbuild } // skip if `overrides.vitest` is `false` if (overrides.vitest !== false && agent === 'pnpm') { overrides['vitest@<3.2.0>vite'] = '^6.3.5' } } } else { overrides.vite ||= `${options.vitePath}/packages/vite` overrides[`@vitejs/plugin-legacy`] ||= `${options.vitePath}/packages/plugin-legacy` const vitePackageInfo = await getVitePackageInfo(options.vitePath) // skip if `overrides.rollup` is `false` if ( vitePackageInfo.dependencies.rollup?.version && overrides.rollup !== false ) { overrides.rollup = vitePackageInfo.dependencies.rollup.version } // apply if `overrides.esbuild` is `true` if ( vitePackageInfo.dependencies.esbuild?.version && overrides.esbuild === true ) { overrides.esbuild = vitePackageInfo.dependencies.esbuild.version } // skip if `overrides.vitest` is `false` if (overrides.vitest !== false && agent === 'pnpm') { overrides['vitest@<3.2.0>vite'] = '^6.3.5' } // build and apply local overrides const localOverrides = await buildOverrides(pkg, options, overrides) cd(dir) // buildOverrides changed dir, change it back overrides = { ...overrides, ...localOverrides, } } if (options.rolldownRelease) { overrides.rolldown = options.rolldownRelease } await applyPackageOverrides(agent, dir, pkg, overrides) await beforeBuildCommand?.(pkg.scripts) await buildCommand?.(pkg.scripts) if (test) { await beforeTestCommand?.(pkg.scripts) await testCommand?.(pkg.scripts) } return { dir } } export async function setupViteRepo(options: Partial) { const repo = options.repo || 'vitejs/vite' await setupRepo({ repo, dir: vitePath, branch: 'main', shallow: true, ...options, }) try { const rootPackageJsonFile = path.join(vitePath, 'package.json') const rootPackageJson = JSON.parse( await fs.promises.readFile(rootPackageJsonFile, 'utf-8'), ) const viteMonoRepoNames = ['@vitejs/vite-monorepo', 'vite-monorepo'] const { name } = rootPackageJson if (!viteMonoRepoNames.includes(name)) { throw new Error( `expected "name" field of ${repo}/package.json to indicate vite monorepo, but got ${name}.`, ) } const needsWrite = await overridePackageManagerVersion( rootPackageJson, 'pnpm', ) if (needsWrite) { fs.writeFileSync( rootPackageJsonFile, JSON.stringify(rootPackageJson, null, 2), 'utf-8', ) if (rootPackageJson.devDependencies?.pnpm) { await $`pnpm install -Dw pnpm --lockfile-only` } } } catch (e) { throw new Error(`Failed to setup vite repo`, { cause: e }) } } export async function getPermanentRef() { cd(vitePath) try { const ref = await $`git log -1 --pretty=format:%H` return ref } catch (e) { console.warn(`Failed to obtain perm ref. ${e}`) return undefined } } export async function buildVite({ verify = false, rolldownRelease, }: { verify?: boolean; rolldownRelease?: string } = {}) { cd(vitePath) const frozenInstall = getCommand('pnpm', 'frozen') const runBuild = getCommand('pnpm', 'run', ['build']) const runTest = getCommand('pnpm', 'run', ['test']) if (rolldownRelease) { const pkgFile = path.join(vitePath, 'package.json') const pkg = JSON.parse(await fs.promises.readFile(pkgFile, 'utf-8')) // Override rolldown in vite's monorepo so it builds against the specified version await applyPackageOverrides('pnpm', vitePath, pkg, { rolldown: rolldownRelease, }) console.log(`overridden rolldown in vite repo with ${rolldownRelease}`) } else { await $`${serializeCommand(frozenInstall)}` } await $`${serializeCommand(runBuild)}` if (verify) { await $`${serializeCommand(runTest)}` } } export async function bisectVite( good: string, runSuite: () => Promise, ) { // sometimes vite build modifies files in git, e.g. LICENSE.md // this would stop bisect, so to reset those changes const resetChanges = async () => $`git reset --hard HEAD` try { cd(vitePath) await resetChanges() await $`git bisect start` await $`git bisect bad` await $`git bisect good ${good}` let bisecting = true while (bisecting) { const commitMsg = await $`git log -1 --format=%s` const isNonCodeCommit = commitMsg.match(/^(?:release|docs)[:(]/) if (isNonCodeCommit) { await $`git bisect skip` continue // see if next commit can be skipped too } const error = await runSuite() cd(vitePath) await resetChanges() const bisectOut = await $`git bisect ${error ? 'bad' : 'good'}` bisecting = bisectOut.substring(0, 10).toLowerCase() === 'bisecting:' // as long as git prints 'bisecting: ' there are more revisions to test } } catch (e) { console.log('error while bisecting', e) } finally { try { cd(vitePath) await $`git bisect reset` } catch (e) { console.log('Error while resetting bisect', e) } } } function isLocalOverride(v: string): boolean { if (!v.includes('/') || v.startsWith('@')) { // not path-like (either a version number or a package name) return false } try { return !!fs.lstatSync(v)?.isDirectory() } catch (e) { if (e.code !== 'ENOENT') { throw e } return false } } /** * utility to override packageManager version * * @param pkg parsed package.json * @param pm package manager to override eg. `pnpm` * @returns {boolean} true if pkg was updated, caller is responsible for writing it to disk */ async function overridePackageManagerVersion( pkg: { [key: string]: any }, pm: string, ): Promise { const versionInUse = pkg.packageManager?.startsWith(`${pm}@`) ? pkg.packageManager.substring(pm.length + 1) : await $`${pm} --version` let overrideWithVersion: string | null = null if (pm === 'pnpm') { if (semver.eq(versionInUse, '7.18.0')) { // avoid bug with absolute overrides in pnpm 7.18.0 overrideWithVersion = '7.18.1' } } if (overrideWithVersion) { console.warn( `detected ${pm}@${versionInUse} used in ${pkg.name}, changing pkg.packageManager and pkg.engines.${pm} to enforce use of ${pm}@${overrideWithVersion}`, ) // corepack reads this and uses pnpm @ newVersion then pkg.packageManager = `${pm}@${overrideWithVersion}` if (!pkg.engines) { pkg.engines = {} } pkg.engines[pm] = overrideWithVersion if (pkg.devDependencies?.[pm]) { // if for some reason the pm is in devDependencies, that would be a local version that'd be preferred over our forced global // so ensure it here too. pkg.devDependencies[pm] = overrideWithVersion } return true } return false } export async function applyPackageOverrides( agent: (typeof AGENTS)[number], dir: string, pkg: any, overrides: Overrides = {}, ) { const useFileProtocol = (v: string) => isLocalOverride(v) ? `file:${path.resolve(v)}` : v // remove boolean flags overrides = Object.fromEntries( Object.entries(overrides) //eslint-disable-next-line @typescript-eslint/no-unused-vars .filter(([key, value]) => typeof value === 'string') .map(([key, value]) => [key, useFileProtocol(value as string)]), ) await $`git clean -fdxq` // remove current install // Remove version from agent string: // yarn@berry => yarn // pnpm@6, pnpm@7 => pnpm const pm = agent?.split('@')[0] await overridePackageManagerVersion(pkg, pm) if (pm === 'pnpm') { const overridesWithoutSpecialSyntax = Object.fromEntries( Object.entries(overrides) //eslint-disable-next-line @typescript-eslint/no-unused-vars .filter(([key, value]) => !key.includes('>')), ) if (!pkg.devDependencies) { pkg.devDependencies = {} } pkg.devDependencies = { ...pkg.devDependencies, ...overridesWithoutSpecialSyntax, // overrides must be present in devDependencies or dependencies otherwise they may not work } if (!pkg.pnpm) { pkg.pnpm = {} } pkg.pnpm.overrides = { ...pkg.pnpm.overrides, ...overrides, } // check `overrides` in pnpm-workspace.yaml const pnpmWorkspaceFile = path.join(dir, 'pnpm-workspace.yaml') if (fs.existsSync(pnpmWorkspaceFile)) { let content = await fs.promises.readFile(pnpmWorkspaceFile, 'utf-8') let modified = false if (/^overrides:/m.test(content)) { delete pkg.pnpm.overrides // remove pnpm.overrides from package.json so that pnpm-workspace.yaml's one is used // merge with existing overrides const output = await $`pnpm config list --json --location project` const currentOverrides = JSON.parse(output).overrides const mergedOverrides = { ...currentOverrides, ...overrides } // replace all indented lines in `overrides` section content = content.replace( /^overrides:\n((?:[ \t]+.+\n)*)/m, () => `overrides:\n${Object.entries(mergedOverrides) .map( ([name, version]) => ` ${JSON.stringify(name)}: ${JSON.stringify(version)}\n`, ) .join('')}`, ) modified = true } if (content.includes('minimumReleaseAge:')) { // disable with comment to avoid error on installation if ecosystem-ci overrides pull in violating updates content = content.replace( /^([ \t]*minimumReleaseAge[ \t]*:[ \t]*\d+[^\r\n]*)$/m, '# $1 -- disabled by ecosystem-ci', ) modified = true } if (content.includes('blockExoticSubdeps:')) { // disable with comment to avoid error on installation if ecosystem-ci overrides pull in tarball URLs content = content.replace( /^([ \t]*blockExoticSubdeps[ \t]*:[ \t]*\w+[^\r\n]*)$/m, '# $1 -- disabled by ecosystem-ci', ) modified = true } if (modified) { await fs.promises.writeFile(pnpmWorkspaceFile, content, 'utf-8') } } } else if (pm === 'yarn') { pkg.resolutions = { ...pkg.resolutions, ...overrides, } } else if (pm === 'npm') { pkg.overrides = { ...pkg.overrides, ...overrides, } // npm does not allow overriding direct dependencies, force it by updating the blocks themselves for (const [name, version] of Object.entries(overrides)) { if (pkg.dependencies?.[name]) { pkg.dependencies[name] = version } if (pkg.devDependencies?.[name]) { pkg.devDependencies[name] = version } } } else { throw new Error(`unsupported package manager detected: ${pm}`) } const pkgFile = path.join(dir, 'package.json') await fs.promises.writeFile(pkgFile, JSON.stringify(pkg, null, 2), 'utf-8') // use of `ni` command here could cause lockfile violation errors so fall back to native commands that avoid these if (pm === 'pnpm') { await $`pnpm install --prefer-frozen-lockfile --strict-peer-dependencies false` } else if (pm === 'yarn') { await $`yarn install` } else if (pm === 'npm') { await $`npm install` } } export function dirnameFrom(url: string) { return path.dirname(fileURLToPath(url)) } export function parseViteMajor(vitePath: string): number { const content = fs.readFileSync( path.join(vitePath, 'packages', 'vite', 'package.json'), 'utf-8', ) const pkg = JSON.parse(content) return parseMajorVersion(pkg.version) } export function parseMajorVersion(version: string) { return parseInt(version.split('.', 1)[0], 10) } async function buildOverrides( pkg: any, options: RunOptions, repoOverrides: Overrides, ) { const { root } = options const buildsPath = path.join(root, 'builds') const buildFiles: string[] = fs .readdirSync(buildsPath) .filter((f: string) => !f.startsWith('_') && f.endsWith('.ts')) .map((f) => path.join(buildsPath, f)) const buildDefinitions: { packages: { [key: string]: string } build: (options: RunOptions) => Promise<{ dir: string }> dir?: string }[] = await Promise.all(buildFiles.map((f) => import(pathToFileURL(f).href))) const deps = new Set([ ...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {}), ...Object.keys(pkg.peerDependencies ?? {}), ]) const needsOverride = (p: string) => repoOverrides[p] === true || (deps.has(p) && repoOverrides[p] == null) const buildsToRun = buildDefinitions.filter(({ packages }) => Object.keys(packages).some(needsOverride), ) const overrides: Overrides = {} for (const buildDef of buildsToRun) { const { dir } = await buildDef.build({ root: options.root, workspace: options.workspace, vitePath: options.vitePath, viteMajor: options.viteMajor, skipGit: options.skipGit, release: options.release, verify: options.verify, // do not pass along scripts }) for (const [name, path] of Object.entries(buildDef.packages)) { if (needsOverride(name)) { overrides[name] = `${dir}/${path}` } } } return overrides } /** * use pnpm ls to get information about installed dependency versions of vite * @param vitePath - workspace vite root */ async function getVitePackageInfo(vitePath: string): Promise { try { // run in vite dir to avoid package manager mismatch error from corepack const current = cwd cd(`${vitePath}/packages/vite`) const lsOutput = $`pnpm ls --json` cd(current) const lsParsed = JSON.parse(await lsOutput) return lsParsed[0] as PackageInfo } catch (e) { console.error('failed to retrieve vite package infos', e) throw e } }