Skip to content

test: install local vite-plus builds through a local npm registry #3233

test: install local vite-plus builds through a local npm registry

test: install local vite-plus builds through a local npm registry #3233

Workflow file for this run

name: Test vp create
permissions: {}
on:
workflow_dispatch:
push:
branches:
- main
paths:
- 'packages/cli/src/create/**'
- 'packages/cli/templates/**'
- 'packages/cli/src/migration/**'
- '.github/workflows/test-vp-create.yml'
pull_request:
types: [opened, synchronize, labeled]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.ref_name != 'main' }}
defaults:
run:
shell: bash
jobs:
detect-changes:
runs-on: namespace-profile-linux-x64-default
permissions:
contents: read
pull-requests: read
outputs:
related-files-changed: ${{ steps.filter.outputs.related-files }}
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
related-files:
- 'packages/cli/src/create/**'
- 'packages/cli/templates/**'
- 'packages/cli/src/migration/**'
- .github/workflows/test-vp-create.yml
download-previous-rolldown-binaries:
needs: detect-changes
runs-on: namespace-profile-linux-x64-default
# Run if: not a PR, OR PR has 'test: create-e2e' label, OR PR is from the deps/upstream-update or a renovate/** dependency branch, OR create-related files changed
if: >-
github.event_name != 'pull_request' ||
contains(github.event.pull_request.labels.*.name, 'test: create-e2e') ||
github.head_ref == 'deps/upstream-update' ||
startsWith(github.head_ref, 'renovate/') ||
needs.detect-changes.outputs.related-files-changed == 'true'
permissions:
contents: read
packages: read
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
- uses: ./.github/actions/download-rolldown-binaries
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
build:
name: Build vite-plus packages
runs-on: namespace-profile-linux-x64-default
permissions:
contents: read
packages: read
needs:
- download-previous-rolldown-binaries
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
- uses: ./.github/actions/clone
- uses: oxc-project/setup-rust@68c3199c5339f965e6e163924c3c450773eba42b # main (pending v1.0.17 — Swatinem/rust-cache v2.9.1 for node24)
with:
save-cache: ${{ github.ref_name == 'main' }}
cache-key: create-e2e-build
- uses: oxc-project/setup-node@4c588e9266bd930b6ddc34307df0659ed511d187 # v1.3.1
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rolldown-binaries
path: ./rolldown/packages/rolldown/src
merge-multiple: true
- name: Build with upstream
uses: ./.github/actions/build-upstream
with:
target: x86_64-unknown-linux-gnu
- name: Pack packages into tgz
run: |
mkdir -p tmp/tgz
# The test jobs serve these tgz through a local npm registry
# (packages/tools/src/local-npm-registry.ts), so `vp create` pins
# and installs them like a real release, with every package manager
# (including bun) resolving the standard
# `vite -> npm:@voidzero-dev/vite-plus-core@<version>` alias. Pin
# both packages to 0.0.0 so a correctly installed local build is
# always distinguishable from any published version, even on a
# release commit that leaves packages/{core,cli} at a published
# version.
(cd packages/core && npm pkg set version=0.0.0)
(cd packages/cli && npm pkg set version=0.0.0)
cd packages/core && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
cd packages/cli && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
# Copy vp binary for test jobs
cp target/x86_64-unknown-linux-gnu/release/vp tmp/tgz/vp
ls -la tmp/tgz
- name: Upload tgz artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: vite-plus-packages
path: tmp/tgz/
retention-days: 1
test-vp-create:
name: vp create ${{ matrix.template.name }} (${{ matrix.package-manager }})
runs-on: namespace-profile-linux-x64-default
permissions:
contents: read
needs:
- build
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
template:
- name: monorepo
create-args: vite:monorepo --directory test-project
template-args: ''
verify-command: vp run ready
verify-migration: 'false'
- name: application
create-args: vite:application --directory test-project
template-args: '-- --template vanilla-ts'
verify-command: vp run build
verify-migration: 'false'
- name: library
create-args: vite:library --directory test-project
template-args: ''
verify-command: |
vp run build
vp run test
verify-migration: 'false'
# Remote template that ships ESLint (+ an eslint.config.js importing
# @eslint/js etc.). Exercises the migrate-before-rewrite reorder in
# `vp create`: after scaffold, ESLint → oxlint and Prettier → oxfmt
# run before the vite-plus rewrite so `.oxlintrc` / `.oxfmtrc` get
# merged into vite.config.ts.
- name: remote-vite-react-ts
create-args: vite@9.0.5
template-args: '-- test-project --template react-ts'
verify-command: vp run build
verify-migration: 'true'
package-manager:
- pnpm
- npm
- yarn
- bun
env:
# The local registry serves the packed 0.0.0 build, so create pins the
# exact version like a real release. The vp binary was built before the
# pack step pinned the package versions, so align the version explicitly.
VP_VERSION: '0.0.0'
# Force full dependency rewriting so the library template's existing
# vite-plus dep gets overridden with the local build
VP_FORCE_MIGRATE: '1'
# yarn 4 quarantines packages published within `npmMinimalAgeGate`
# (default 1440 min / 24h). When an oxlint bump landed <24h ago, the
# migration step's `vp dlx @oxlint/migrate@<bundled oxlint>` fails with
# `YN0016 ... are quarantined`. The migrate tool is version-pinned to the
# bundled oxlint, so disable the gate for this test (no-op for npm/pnpm/bun).
YARN_NPM_MINIMAL_AGE_GATE: '0'
steps:
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- name: Download vite-plus packages
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: vite-plus-packages
path: tmp/tgz
- name: Install vp CLI
run: |
mkdir -p target/release
cp tmp/tgz/vp target/release/vp
chmod +x target/release/vp
node $GITHUB_WORKSPACE/packages/tools/src/install-global-cli.ts --tgz $GITHUB_WORKSPACE/tmp/tgz/vite-plus-0.0.0.tgz
echo "$HOME/.vite-plus/bin" >> $GITHUB_PATH
- name: Verify vp installation
run: |
which vp
vp --version
- name: Start local npm registry
# Serve the packed local build behind a real registry interface for
# the create's install and every later step (the registry env is
# exported via GITHUB_ENV; the detached server lives for the job).
# Both output fds go to a file, not this step's pipes, so the step
# completes while the server keeps running.
run: |
node "$GITHUB_WORKSPACE/packages/tools/src/local-npm-registry.ts" --serve --packages-dir "$GITHUB_WORKSPACE/tmp/tgz" > "$RUNNER_TEMP/local-registry.out" 2>&1 &
server_pid=$!
until grep -q '"registry"' "$RUNNER_TEMP/local-registry.out" 2>/dev/null; do
if ! kill -0 "$server_pid" 2>/dev/null; then
echo "local registry failed to start:"
cat "$RUNNER_TEMP/local-registry.out"
exit 1
fi
sleep 0.2
done
handshake=$(head -1 "$RUNNER_TEMP/local-registry.out")
echo "$handshake"
echo "$handshake" | node -e 'const { env } = JSON.parse(require("node:fs").readFileSync(0, "utf8")); for (const [k, v] of Object.entries(env)) console.log(`${k}=${v}`)' >> "$GITHUB_ENV"
# No VP_OVERRIDE_PACKAGES: with VP_VERSION=0.0.0 the product default
# override map already pins `vite` to npm:@voidzero-dev/vite-plus-core@0.0.0
# and `vitest` to the bundled version (see VITE_PLUS_OVERRIDE_PACKAGES in
# packages/cli/src/utils/constants.ts), and stays correct across vitest
# bumps without editing this workflow.
- name: Run vp create ${{ matrix.template.name }} with ${{ matrix.package-manager }}
working-directory: ${{ runner.temp }}
run: |
vp create ${{ matrix.template.create-args }} \
--no-interactive \
--no-agent \
--package-manager ${{ matrix.package-manager }} \
${{ matrix.template.template-args }}
- name: Verify project structure
working-directory: ${{ runner.temp }}/test-project
run: |
# package.json must exist
test -f package.json
echo "✓ package.json exists"
cat package.json
# List all files for debugging
echo "--- Project root files ---"
ls -la
# Check correct lockfile exists
case "${{ matrix.package-manager }}" in
pnpm)
test -f pnpm-lock.yaml
echo "✓ pnpm-lock.yaml exists"
;;
npm)
test -f package-lock.json
echo "✓ package-lock.json exists"
;;
yarn)
test -f yarn.lock
echo "✓ yarn.lock exists"
;;
bun)
if [ -f bun.lock ]; then
echo "✓ bun.lock exists"
elif [ -f bun.lockb ]; then
echo "✓ bun.lockb exists"
else
echo "✗ No bun lockfile found"
exit 1
fi
;;
esac
# node_modules must exist (vp install ran successfully)
test -d node_modules
echo "✓ node_modules exists"
# Monorepo-specific checks
if [ "${{ matrix.template.name }}" = "monorepo" ]; then
test -d apps/website
echo "✓ apps/website exists"
test -d packages/utils
echo "✓ packages/utils exists"
case "${{ matrix.package-manager }}" in
pnpm)
test -f pnpm-workspace.yaml
echo "✓ pnpm-workspace.yaml exists"
;;
yarn)
test -f .yarnrc.yml
echo "✓ .yarnrc.yml exists"
;;
esac
fi
- name: Verify single dependency instances (pnpm only)
if: matrix.package-manager == 'pnpm'
working-directory: ${{ runner.temp }}/test-project
run: |
# The `vite` override must dedupe vite-plus / vite / vitest to a single
# instance each. When a peer variation splits the graph (e.g. an upstream
# `vite` auto-installed to satisfy vitest's peer in a package without a
# direct `vite` dep), `vp why` reports multiple instances. Detection:
# - vite-plus / vitest: a split prints "Found 1 version, N instances of <pkg>".
# - vite: it is overridden to @voidzero-dev/vite-plus-core, so a clean tree
# only summarises that package; a standalone upstream copy adds a
# "Found <n> version(s) of vite" line.
# Regression guard for voidzero-dev/vite-plus#1932 (the pnpm dedupe fix).
# `-r` checks every workspace package, not just the root importer, so a
# duplicate confined to a sub-package (apps/website, packages/utils) is
# still caught.
fail=0
check() {
pkg="$1"; pattern="$2"
out=$(vp why -r "$pkg" 2>&1)
found=$(echo "$out" | grep '^Found' || true)
echo "[$pkg]"; echo "$found"
if echo "$found" | grep -qE "$pattern"; then
echo "✗ $pkg is not a single instance (override did not dedupe under pnpm)"
echo "----- full \`vp why -r $pkg\` output -----"
echo "$out"
echo "---------------------------------------"
fail=1
else
echo "✓ $pkg single instance"
fi
}
check vite-plus 'instances of vite-plus'
check vitest 'instances of vitest'
check vite 'of vite$'
if [ "$fail" -ne 0 ]; then
echo "Expected vite-plus, vite, and vitest to each resolve to a single instance."
exit 1
fi
echo "✓ vite-plus, vite, vitest are all single instances"
- name: Verify local packages installed
working-directory: ${{ runner.temp }}/test-project
run: |
node -e "
const path = require('path');
const pkg = require(path.resolve('node_modules/vite-plus/package.json'));
if (pkg.version !== '0.0.0') {
console.error('Expected vite-plus@0.0.0, got ' + pkg.version);
process.exit(1);
}
console.log('✓ vite-plus@' + pkg.version + ' installed correctly');
"
- name: Verify monorepo sub-package deps
if: matrix.template.name == 'monorepo'
working-directory: ${{ runner.temp }}/test-project
env:
PACKAGE_MANAGER: ${{ matrix.package-manager }}
run: |
# Issue 1: packages/utils inherits `vite-plus: ^x.y.z` from the
# library template. In catalog-supporting monorepos (pnpm/yarn/bun)
# the migrator must normalize it so siblings don't drift.
# Issue 2: apps/website is scaffolded by create-vite which ships
# `vite` (and sometimes `vitest`) in devDependencies. After
# migration the scripts are rewritten to `vp ...` and `vite-plus`
# brings the runtime in transitively. For npm/yarn/bun those keys
# are dropped (the root overrides/resolutions redirect the
# transitive/peer vite to @voidzero-dev/vite-plus-core regardless).
# For pnpm the aliased `vite` is kept on purpose: pnpm only surfaces
# the pnpm-workspace.yaml `overrides.vite: catalog:` entry through a
# package that directly depends on `vite`, so dropping it would make
# `vp why vite` report upstream vite and the override look ineffective.
node -e "
const fs = require('fs');
const pm = process.env.PACKAGE_MANAGER;
for (const f of ['apps/website/package.json', 'packages/utils/package.json']) {
if (!fs.existsSync(f)) {
console.error('✗ expected ' + f + ' to exist after vp create vite:monorepo');
process.exit(1);
}
}
const app = JSON.parse(fs.readFileSync('apps/website/package.json', 'utf8'));
const utils = JSON.parse(fs.readFileSync('packages/utils/package.json', 'utf8'));
const appDev = app.devDependencies || {};
const utilsDev = utils.devDependencies || {};
if (pm === 'pnpm') {
if (!appDev['vite']) {
console.error('✗ pnpm apps/website should keep aliased vite so the workspace override stays effective');
process.exit(1);
}
console.log('✓ pnpm apps/website keeps aliased vite');
} else {
for (const name of ['vite', 'vitest']) {
if (appDev[name]) {
console.error('✗ apps/website devDependencies still has ' + name + ': ' + appDev[name]);
process.exit(1);
}
}
console.log('✓ apps/website devDependencies has no vite/vitest');
}
if (!appDev['vite-plus']) {
console.error('✗ apps/website missing vite-plus devDependency');
process.exit(1);
}
if (!utilsDev['vite-plus']) {
console.error('✗ packages/utils missing vite-plus devDependency');
process.exit(1);
}
if (pm !== 'npm' && appDev['vite-plus'] !== utilsDev['vite-plus']) {
console.error('✗ vite-plus spec drift: apps/website=' + appDev['vite-plus'] + ' packages/utils=' + utilsDev['vite-plus']);
process.exit(1);
}
console.log('✓ vite-plus consistent across sub-packages: app=' + appDev['vite-plus'] + ' utils=' + utilsDev['vite-plus']);
"
- name: Verify ESLint/Prettier auto-migration
if: matrix.template.verify-migration == 'true'
working-directory: ${{ runner.temp }}/test-project
run: |
# eslint.config.js must be gone (migration deleted it)
test ! -f eslint.config.js
echo "✓ eslint.config.js removed"
# .oxlintrc.json must NOT be loose on disk — it was merged into
# vite.config.ts by the rewrite step that runs after migration.
test ! -f .oxlintrc.json
echo "✓ .oxlintrc.json merged into vite.config.ts"
# vite.config.ts must contain the merged oxlint config.
grep -q '^[[:space:]]*lint:' vite.config.ts
echo "✓ vite.config.ts has merged lint section"
# package.json: eslint devDep removed, vite-plus present, lint script rewritten.
node -e "
const pkg = require('./package.json');
if (pkg.devDependencies && pkg.devDependencies.eslint) {
console.error('✗ eslint devDependency should have been removed');
process.exit(1);
}
if (!pkg.devDependencies || !pkg.devDependencies['vite-plus']) {
console.error('✗ vite-plus devDependency missing');
process.exit(1);
}
if (!pkg.scripts || !pkg.scripts.lint || !pkg.scripts.lint.includes('vp lint')) {
console.error('✗ lint script should invoke vp lint, got: ' + (pkg.scripts && pkg.scripts.lint));
process.exit(1);
}
console.log('✓ package.json migrated (eslint gone, vite-plus added, lint script rewritten)');
"
- name: Run vp check
working-directory: ${{ runner.temp }}/test-project
run: vp check
- name: Verify project builds
working-directory: ${{ runner.temp }}/test-project
run: ${{ matrix.template.verify-command }}
- name: Verify cache (monorepo only)
if: matrix.template.name == 'monorepo'
working-directory: ${{ runner.temp }}/test-project
run: |
# Under npm and yarn, `vp run ready` reaches 100% cache hit only on
# the third invocation (#1638): vite-task's directory-listing
# fingerprint and fspy read/write tracking surface false-positive
# misses on run #2 because `packages/utils/node_modules/` is born
# during run #1. pnpm and bun pre-create per-package
# `node_modules/` at install time and reach 100% on run #2; Yarn
# Berry did too under the old file: overrides (per-package copies),
# but with registry-resolved semver specs it hoists the workspace
# deps like a real install, so it now behaves like npm here.
# The preceding `Verify project builds` step already invoked
# `vp run ready` once (verify-command for monorepo), so one
# extra warm-up here is enough.
case "${{ matrix.package-manager }}" in
npm|yarn)
vp run ready >/dev/null 2>&1
;;
esac
output=$(vp run ready 2>&1)
echo "$output"
if ! echo "$output" | grep -q 'cache hit (100%)'; then
echo "✗ Expected 100% cache hit"
echo "--- vp run --last-details (cache-miss diagnostics) ---"
vp run --last-details || true
exit 1
fi
echo "✓ 100% cache hit verified"