Skip to content

Commit 327d23a

Browse files
authored
feat(migrate): upgrade existing Vite+ projects across versions (#1891)
RFC: `rfcs/migrate-existing-projects.md` ## Problem `vp migrate` on an existing v0.1.x Vite+ project did not upgrade cleanly: it delegated to the stale local CLI, left `pnpm-workspace.yaml` overrides and catalogs pinning `vite`/`vitest` to old versions, and skewed coverage providers. The v0.2.x release notes currently tell users not to run `vp migrate` yet. ## What it does - **Routing**: when the local `vite-plus` is older than the global `vp`, run migrate from the global CLI so the stale local CLI does not run. - **Re-pin** `vite-plus` and the `vite` -> `@voidzero-dev/vite-plus-core` alias off 0.1.x, across dependencies, overrides/resolutions, and catalogs, for every workspace package rather than only the root manifest. - **vitest by usage**: removed in the common case (vite-plus provides it transitively), kept and ecosystem-aligned when a package uses it directly or via a range-peer integration; align `@vitest/*` (coverage-v8/-istanbul, ui, web-worker) to the bundled version, with the browser providers kept opt-in. - **pnpm bootstrap**: move `package.json#pnpm` settings into `pnpm-workspace.yaml`, fix the empty `"pnpm": {}` misrouting that left stale overrides, inject the direct `vite` edge where pnpm needs it, and approve required build scripts. The "pending" check scans every workspace package's catalog ref, so a package pinned to a stale named catalog (`catalog:legacy`) is not missed. - **Plugin packages**: preserve real `vite`/`vitest` imports in `vite-plugin-*` and unplugin packages instead of rewriting them to `vite-plus`. - **Summary**: the migrate output leads with `Updated . to Vite+ <version>` (or `Migrated` for a fresh project) and lists the toolchain version changes as a `from -> to` table for `vite-plus`, the raw `vite` version, and `vitest` when present. - **Progress**: on a large monorepo the per-package config-compatibility check reports live progress (`Checking config compatibility (N/total): <package>`), so the spinner keeps moving and shows what it is processing instead of stalling on a stale message. ## Version-only by default, `--full` for setup On an existing Vite+ project, `vp migrate` does the toolchain version upgrade above and **silently skips the first-time setup bucket** (git hooks, editor config, agent files, ESLint/Prettier migration, framework shims, tsconfig `baseUrl`, and `.nvmrc`/Volta to `.node-version`), so a version bump does not re-touch things you already set up. Pass `--full` to run the whole setup, or a per-action flag (`--hooks` / `--agent` / `--editor`) to opt into a single action. A bare upgrade that left setup actions on the table hints at `vp migrate --full`. Fresh (non Vite+) projects always run the full migration. ```console # default: toolchain version upgrade only $ vp migrate --no-interactive ◇ Updated . to Vite+ 0.2.1 • Node 22.18.0 pnpm 10.18.0 • Dependencies: vite-plus 0.1.21 → 0.2.1 vite → 8.1.0 • Package manager settings configured • Skipped editor, hooks, and lint setup. Run `vp migrate --full` to apply them. # --full: upgrade plus the setup bucket $ vp migrate --full --no-interactive ◇ Updated . to Vite+ 0.2.1 • Node 22.18.0 pnpm 10.18.0 • Dependencies: vite-plus 0.1.21 → 0.2.1 vite → 8.1.0 • 2 config updates applied • Node version manager file migrated to .node-version • Package manager settings configured # --hooks: upgrade plus a single setup action (no --full) $ vp migrate --hooks --no-interactive ◇ Updated . to Vite+ 0.2.1 • Node 22.18.0 pnpm 10.18.0 • Dependencies: vite-plus 0.1.21 → 0.2.1 vite → 8.1.0 • Git hooks configured • Package manager settings configured ``` On a large monorepo the spinner walks the per-package config-compatibility check, so the run is never silent: ```console ◒ Checking config compatibility (7/42): examples/tpr-demo ◒ Checking config compatibility (23/42): tests/fixtures/pages-basic ◒ Checking config compatibility (40/42): tests/fixtures/ecosystem/next-themes ``` ## Node.js version The native binding runs on any Node `>=20.0.0`, and the published `@voidzero-dev/vite-plus-*` platform packages now declare that ABI floor (the `vite-plus` and core packages keep their wider `engines.node` support range). So a Node pin whose floor is below the support range but at or above `20.0.0` (`engines.node: 24.x`, `>=22`, `.node-version` `24.3.0`, ...) still installs the native binding and is left untouched by migration. Converting `.nvmrc` and Volta `volta.node` to `.node-version` (and repointing any `actions/setup-node` `node-version-file: .nvmrc` reference in workflows and composite actions, `.github/actions/**/action.{yml,yaml}`, so CI does not break) is part of the `--full` setup, not the default upgrade. ## Manual pkg.pr.new testing Install an isolated pkg.pr.new global CLI and run the PR's `vp migrate` against a local project. Dependencies resolve through the registry bridge as ordinary `0.0.0-commit.<sha>` versions, persisted into the project's `.npmrc` (and `.yarnrc.yml` for Yarn Berry) so its own CI installs the same build: ```bash ./.github/scripts/test-pkg-pr-new-migrate.sh 1891 /path/to/project [--no-interactive] ``` The first argument accepts a PR number or commit SHA. The helper keeps `~/.vite-plus` untouched, forces migration through the preview CLI even when the project has a same-version local CLI, clears only the workspace root lockfile and `node_modules` before migrating, and refuses dirty Git worktrees unless `ALLOW_DIRTY=1`. Confirm the result with `vp why -r vite-plus vite vitest` (each must resolve to a single version).
1 parent ff70707 commit 327d23a

404 files changed

Lines changed: 21071 additions & 7506 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
name: test-pkg-pr-new-migrate
3+
description: Verify a pkg.pr.new build of vite-plus against a real project before release — run `vp migrate` from the pkg.pr.new commit against a local project, deps resolved through the registry bridge. Use when asked to verify/e2e-test a pkg.pr.new build against a project, "test PR #<N> on <project>", or check a prerelease against a repo.
4+
allowed-tools: Bash, Read
5+
---
6+
7+
# Verify a pkg.pr.new build against one project
8+
9+
Installs an isolated global `vp` built from a registry bridge commit build and runs `vp migrate` on a specified local project. The global CLI and the migrated project both pin `vite-plus`/`vite` to the clearly-defined `0.0.0-commit.<sha>` build. `vp migrate` itself writes the bridge registry into the project's `.npmrc` (or `.yarnrc.yml` for Yarn Berry) so the deps resolve, during this run and in the project's own CI; this script only force-stages that file past `.gitignore`.
10+
11+
Required inputs: a `<PR-or-SHA>` (the build to verify) and a `<project-path>`. If either is missing from the request, **ask the user** for it — never guess the PR/SHA, as the build under test is the user's choice.
12+
13+
```bash
14+
.github/scripts/test-pkg-pr-new-migrate.sh <PR-or-SHA> <project-path> [migrate-options...]
15+
# e.g.
16+
.github/scripts/test-pkg-pr-new-migrate.sh 1891 /path/to/npmx.dev --no-interactive
17+
```
18+
19+
- First arg is a PR number or commit SHA; the script resolves the immutable commit via the bridge `x-commit-key` header and verifies the bridge serves it (the pkg.pr.new publish workflow registers each commit).
20+
- Never touches `~/.vite-plus`; clears only the workspace ROOT lockfile + `node_modules` before migrating; refuses a dirty worktree unless `ALLOW_DIRTY=1`; prints the project's `git status`/`diff` at the end — inspect that to confirm the migration result.
21+
22+
**The build under test must include the "migrate writes the bridge registry" feature** (this session's work / current branch head onward). The harness no longer writes the registry itself — it relies on `vp migrate` doing it. Testing an older build with this harness would leave the project with no bridge registry, so its deps resolve from npmjs (`ERR_PNPM_NO_MATCHING_VERSION` on the `0.0.0-commit.<sha>` version). Always verify a fresh build of the branch, not a stale published commit.
23+
24+
The script prints the resolved versions at the end, querying one package at a time (npm/yarn/bun `why` only accept a single package; `-r` is pnpm-only). Each must resolve to exactly ONE version: `vite-plus` and `vite` at the expected `0.0.0-commit.<sha>` (vite via the `@voidzero-dev/vite-plus-core` alias), `vitest` at the bundled upstream version. Multiple versions, or a stale/wrong version, means the migration or install is broken. To re-check by hand:
25+
26+
```bash
27+
cd <project-path>
28+
# pnpm: one call, recursive across workspaces
29+
vp why -r vite-plus vite vitest
30+
# npm / yarn / bun: one package per call, no -r
31+
vp why vite-plus && vp why vite && vp why vitest
32+
```
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
name: verify-interactive-cli
3+
description: Drive and capture vp's interactive (clack) prompts in a tmux session to verify interactive UX and catch spinner-over-prompt bugs that snap tests (which run non-interactively) miss. Use when asked to test/verify/capture an interactive vp command's prompts (vp migrate, vp create, ...), reproduce a prompt-rendering bug, or show the real interactive CLI output for a PR.
4+
allowed-tools: Bash, Read
5+
---
6+
7+
# Verify vp's interactive CLI prompts
8+
9+
Snap tests run `vp` non-interactively, so interactive clack prompts (hooks / agent / editor confirms, the Node-version upgrade confirm) and TTY-only rendering bugs are never exercised. This skill drives the real prompts in a TTY (via tmux), captures clean output, and can catch a spinner animating underneath an active prompt (a real, recurring UX bug class, e.g. the "Preparing migration" and "Checking Node.js version support" spinners).
10+
11+
## Prerequisites
12+
13+
- `tmux` (macOS has none by default): `brew install tmux`.
14+
- The global `vp` must contain the code under test. To exercise working-tree changes, rebuild first: `pnpm bootstrap-cli` (~5 min), then confirm with `vp --version`.
15+
16+
## Driver
17+
18+
`interactive-cli-tmux-driver.sh` is bundled in this skill's directory.
19+
20+
```bash
21+
# Run to completion, auto-accepting every prompt's DEFAULT; prints a clean transcript.
22+
.claude/skills/verify-interactive-cli/interactive-cli-tmux-driver.sh <project-dir> "vp migrate"
23+
24+
# STOP at a specific prompt (do NOT answer it) and check for a spinner animating under it.
25+
.claude/skills/verify-interactive-cli/interactive-cli-tmux-driver.sh <project-dir> "vp migrate" "Upgrade Node.js"
26+
```
27+
28+
How it works:
29+
30+
- Runs the command in a detached tmux session; `tmux capture-pane -p` yields clean text (clack's in-place redraws overwrite, so only resolved lines remain), far cleaner than `expect`'s raw ANSI capture.
31+
- Auto-accepts each prompt's default by sending Enter when the pane goes STABLE. A waiting prompt is static; animating spinners keep the pane changing, so Enter never fires mid-work.
32+
- End-detection uses `; echo "$M1$M2 exit=$?"` where M1/M2 are split vars, so the literal end marker is not in the typed command line (otherwise a grep matches the echoed command, not the program output).
33+
- With a STOP_AT regex it halts at the target prompt and captures twice ~3s apart: identical captures = static prompt (OK); differing captures (or a `Checking ... (Xs)` line) = a spinner is animating under the prompt = a UX bug.
34+
35+
## Setting up a target project
36+
37+
Create a throwaway project that triggers the prompts you want to see, e.g. a fresh Vite app whose `.node-version` is below the supported range to exercise the Node-upgrade confirm:
38+
39+
```bash
40+
mkdir -p /tmp/vp-demo && cd /tmp/vp-demo
41+
printf '{\n "name":"d","private":true,"type":"module","packageManager":"pnpm@10.18.0",\n "scripts":{"build":"vite build","test":"vitest run"},\n "devDependencies":{"vite":"^8.0.0","vitest":"^4.1.0"}\n}\n' > package.json
42+
echo "24.3.0" > .node-version
43+
printf 'import { defineConfig } from "vite";\nexport default defineConfig({});\n' > vite.config.ts
44+
git init -q && git add -A && git -c user.email=x@x -c user.name=x commit -qm init
45+
```
46+
47+
## When you find a spinner-over-prompt bug
48+
49+
Fix it test-first: the prompt's code must pause the migration progress spinner before the confirm renders. Assert the call order is `['pause', 'confirm']`. Reference fix: `upgradeUnsupportedNodeVersions` (in `packages/cli/src/migration/migrator/setup.ts`) takes a `pauseProgress` callback that `bin.ts` wires to `clearMigrationProgress`, called right before `prompts.confirm`.
50+
51+
## Gotchas
52+
53+
- clack's active-prompt marker here is `` (not always ``); detect a waiting prompt by pane stability, not a specific glyph.
54+
- `VP_SKIP_INSTALL=1` skips the dependency install but breaks steps that load `vite.config.ts` (e.g. the prettier auto-migration) because `vite` isn't installed; use it only when you stop before the install step.
55+
- A `&` inside a background task detaches the script, so the task reports "completed" early while it keeps running; poll a status file or `capture-pane` to track real progress.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/bin/bash
2+
# Drive and capture an interactive `vp` (clack) prompt flow inside tmux, to verify
3+
# interactive UX that snap tests (which run non-interactively) never cover.
4+
#
5+
# Usage:
6+
# interactive-cli-tmux-driver.sh <project-dir> "<command>" [STOP_AT_REGEX]
7+
# interactive-cli-tmux-driver.sh /tmp/demo "vp migrate"
8+
# interactive-cli-tmux-driver.sh /tmp/demo "vp migrate" "Upgrade Node.js"
9+
#
10+
# No STOP_AT : run to completion, auto-accepting every prompt's DEFAULT (Enter),
11+
# then print a clean transcript.
12+
# With STOP_AT: drive prompts until one matching the regex appears, then STOP
13+
# (do NOT answer it) and capture the pane twice 3s apart. Identical
14+
# captures => prompt is static (good). Differing captures (or a
15+
# "Checking ... (Xs)" line) => a spinner is animating UNDER the
16+
# prompt -- a real UX bug (this is how the Node-upgrade confirm
17+
# spinner-overlap was found).
18+
#
19+
# Why tmux and not `expect`: expect's raw capture is full of ANSI cursor/redraw
20+
# noise; `tmux capture-pane -p` returns clean text because in-place redraws
21+
# overwrite and only the resolved lines remain in scrollback.
22+
# macOS has no tmux/timeout by default: `brew install tmux`.
23+
set -u
24+
DIR="${1:?project dir}"; CMD="${2:?command, e.g. \"vp migrate\"}"; STOP_AT="${3:-}"
25+
command -v tmux >/dev/null || { echo "need tmux: brew install tmux" >&2; exit 1; }
26+
S="clicap_$$"
27+
cap1="$(mktemp)"; cap2="$(mktemp)"
28+
29+
tmux kill-session -t "$S" 2>/dev/null
30+
tmux new-session -d -s "$S" -x 100 -y 50
31+
tmux set-option -t "$S" history-limit 50000
32+
# M1/M2 are split so the end marker never appears in the TYPED command line --
33+
# otherwise a grep for it matches the echoed command, not the program output.
34+
tmux send-keys -t "$S" 'export PS1="$ " PROMPT="%% " M1=CLI M2=CAPDONE' Enter; sleep 1
35+
tmux send-keys -t "$S" "cd '$DIR'" Enter; sleep 1
36+
tmux send-keys -t "$S" 'clear' Enter; sleep 1
37+
tmux send-keys -t "$S" "$CMD"'; echo "$M1$M2 exit=$?"' Enter
38+
39+
prev=""; stable=0; sent=0
40+
for i in $(seq 1 180); do
41+
sleep 2
42+
pane="$(tmux capture-pane -t "$S" -p -S -120 2>/dev/null)"
43+
# reached the prompt we want to inspect -> stop without answering it
44+
if [ -n "$STOP_AT" ] && printf '%s' "$pane" | grep -q "$STOP_AT"; then
45+
tmux capture-pane -t "$S" -p -S -60 > "$cap1"; sleep 3
46+
tmux capture-pane -t "$S" -p -S -60 > "$cap2"
47+
if diff -q "$cap1" "$cap2" >/dev/null; then
48+
echo "STATIC prompt (nothing animating underneath) -- OK"
49+
else
50+
echo "ANIMATING under the prompt (likely spinner-over-prompt bug):"
51+
diff "$cap1" "$cap2"
52+
fi
53+
echo "--- prompt ---"; sed -n "/$STOP_AT/,\$p" "$cap1"
54+
tmux kill-session -t "$S" 2>/dev/null; rm -f "$cap1" "$cap2"; exit 0
55+
fi
56+
# program finished
57+
printf '%s' "$pane" | grep -q "CLICAPDONE exit=" && break
58+
# A waiting prompt makes the pane STABLE; animating spinners keep it changing,
59+
# so this won't fire mid-work. Accept the default with Enter, once per state.
60+
if [ "$pane" = "$prev" ]; then
61+
stable=$((stable+1))
62+
if [ "$stable" -ge 2 ] && [ "$sent" -eq 0 ]; then tmux send-keys -t "$S" Enter; sent=1; fi
63+
else
64+
prev="$pane"; stable=0; sent=0
65+
fi
66+
done
67+
68+
echo "=== clean transcript ==="
69+
tmux capture-pane -t "$S" -p -S -3000
70+
tmux kill-session -t "$S" 2>/dev/null; rm -f "$cap1" "$cap2"

0 commit comments

Comments
 (0)