feat(migrate): upgrade existing Vite+ projects across versions#1891
feat(migrate): upgrade existing Vite+ projects across versions#1891fengmk2 wants to merge 114 commits into
Conversation
✅ Deploy Preview for viteplus-preview canceled.
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
feb8068 to
5090afc
Compare
vite-plus
@voidzero-dev/vite-plus-core
@voidzero-dev/vite-plus-prompts
@voidzero-dev/vite-plus-cli-darwin-arm64
@voidzero-dev/vite-plus-cli-darwin-x64
@voidzero-dev/vite-plus-cli-linux-arm64-gnu
@voidzero-dev/vite-plus-cli-linux-arm64-musl
@voidzero-dev/vite-plus-cli-linux-x64-gnu
@voidzero-dev/vite-plus-cli-linux-x64-musl
@voidzero-dev/vite-plus-cli-win32-arm64-msvc
@voidzero-dev/vite-plus-cli-win32-x64-msvc
@voidzero-dev/vite-plus-darwin-arm64
@voidzero-dev/vite-plus-darwin-x64
@voidzero-dev/vite-plus-linux-arm64-gnu
@voidzero-dev/vite-plus-linux-arm64-musl
@voidzero-dev/vite-plus-linux-x64-gnu
@voidzero-dev/vite-plus-linux-x64-musl
@voidzero-dev/vite-plus-win32-arm64-msvc
@voidzero-dev/vite-plus-win32-x64-msvc
commit: |
5090afc to
732edd6
Compare
E2E test projects
× typescript(TS2783): 'staged' is specified more than once, so this usage will be overwritten.
╭─[vite.config.ts:118:3]
117 │ export default defineConfig({
118 │ ╭─▶ staged: {
119 │ │ '*': 'vp check --fix',
120 │ ╰─▶ },
121 │ ...config, // shared lint/fmt/build from @janosh/vite-config (dotfiles)
╰────
✖ bash -c "tsc --noEmit":
error TS2688: Cannot find type definition file for 'vite-plus/client'.
The file is in the program because:
Entry point of type library 'vite-plus/client' specified in compilerOptions
tsconfig.json:12:33
12 "types": ["vitest/globals", "vite-plus/client"],
~~~~~~~~~~~~~~~~~~
File is entry point of type library specified here.
|
This comment was marked as outdated.
This comment was marked as outdated.
7a1d2de to
c68f763
Compare
1bba44c to
911881e
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b1f776c9ed
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…log) Bun resolves catalog: references only inside a workspace, so the upgrade path converting a standalone (non-workspace) bun project's deps to catalog: broke `bun install` with "vite@catalog: failed to resolve". Gate all three bun-catalog sites (the two supportCatalog computations and the rewriteBunCatalog sink) on a real bun workspace, mirroring the fresh path; standalone bun keeps concrete specs and writes no catalog field. Adds a unit test plus a runnable migration-standalone-bun-install snap that runs a real bun install. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
The fixture exercises the bun#8406 direct-vite-edge-as-catalog: behavior but was authored as a standalone bun project, where bun cannot resolve catalogs. After the standalone-bun fix, migrate correctly drops that catalog, breaking the snap. Declare `workspaces` so the catalog scenario stays valid (the standalone case is covered by migration-standalone-bun-install) and regenerate the global snap. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
- Node-pin reachability: an already-Vite+, otherwise up-to-date project whose only pending work was a below-floor Node pin (.node-version / devEngines.runtime / engines.node) hit the "already using Vite+" early return, so upgradeUnsupportedNodeVersions never ran. hasExistingVitePlusMigrationCandidates now detects a pending pin via the existing planNodeVersionUpgrades planner. - pnpm overrides: the ">vite" dependency-selector cleanup deleted entries while iterating the live items array, skipping an adjacent second selector and leaving a stale ">vite" override. Snapshot the keys first (mirrors the loop above); regenerate the affected snap. - Yarn hoisting: the nmHoistingLimits opt-out guard only checked devDependencies, so a workspace declaring vite-plus under dependencies was skipped. Use hasDirectVitePlusInstallEntry (checks both groups). Also format the migration-upgrade-bun-catalog fixture input. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
The "Comment bridge version" step creates/updates a sticky comment via github.rest.issues.createComment, which is gated on issues:write; the create path (a PR without a sticky comment yet) would 403 and be swallowed by continue-on-error. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 36a8160d49
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Migrate rewrote `from 'vite'` to `from 'vite-plus'` in every package, breaking published plugin libraries (e.g. @nkzw/vite-plugin-remdx, unplugin-*) for plain-vite consumers. Also skip the vite rewrite for packages whose unscoped name follows a plugin naming convention (`vite-plugin-` per https://vite.dev/guide/api-plugin, or `unplugin-` per https://unplugin.unjs.io), alongside the existing peer/runtime-dependency skip. Scoped to `vite`; vitest/tsdown rewriting is unchanged; `keywords` is deliberately not used (too broad). Adds Rust unit tests and a migration-preserve-plugin-vite-import snap (vite-plugin and unplugin packages keep `from 'vite'`, a normal app gets `from 'vite-plus'`), and documents the rule in migrate-rules.md and the migrate RFC. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
Drop test_skip_vite_preserves_plugin_import_by_name_convention: the
skip_vite preserve/rewrite behavior it checked is already covered by
test_skip_vite_when_peer_dependency and the crate's default rewrite tests, and
it was misnamed (it hand-built SkipPackages and never exercised the name
convention, that path is covered by the detection tests and the e2e snap).
Removing it also clears the cargo fmt --check failure on its two-line
`let rewritten = SkipPackages { ... }`.
Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
- Yarn catalogs: gate `catalog:` emission on Yarn >= 4.10.0 (new yarnSupportsCatalog, mirroring pnpmSupportsWorkspaceSettings). Older Yarn cannot resolve catalog references, so a below-4.10.0 Yarn project now gets concrete specs in both the fresh (orchestrators.ts) and upgrade (vite-plus-bootstrap.ts) paths, and its .yarnrc.yml omits the catalog field. - Node engines: preserve multi-major disjuncts when lifting a below-floor pin. `^20 || ^22` now becomes `>=20.19.0 <21.0.0 || >=22.18.0 <23.0.0` instead of an open-ended `>=20.19.0` that dropped the 22 branch and admitted 21/23; a fully-unsupported `^21 || ^23` still yields no rewrite. - bunx scripts: only treat a suffix `bunx` as a command launcher when the command is a known runner wrapper (npm/pnpm/yarn/bun/npx/cross-env/dotenv/...), so `echo -- bunx --bun vite build` is left literal. - docs: clarify that bun catalogs only resolve inside a workspace; a standalone bun project keeps concrete specs. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
migration-standalone-yarn-below-catalog pins yarn@3.6.0 (< 4.10.0) and asserts `vp migrate` emits concrete specs (vite via the @voidzero-dev/vite-plus-core alias, vite-plus at the version, concrete resolutions) with no `catalog:` references and no `.yarnrc.yml` catalog field. This is the end-to-end counterpart to the yarn@4.12.0 fixtures that keep catalogs, covering the new yarnSupportsCatalog (>= 4.10.0) gating. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
|
@codex review |
Drop the `= true` defaults on rewriteYarnrcYml and yarnrcSatisfiesVitePlus. `= true` is the unsafe direction (catalog on assumes Yarn >= 4.10.0, the exact case the gating exists to avoid); every caller already passes the argument, so the defaults were dead. Making it a required `supportCatalog: boolean` matches the module convention and forces future yarn callers to decide. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
anyhow < 1.0.103 has an unsoundness in Error::downcast_mut() (UB when downcast_mut is called after Error::context). cargo-deny in the Security Analysis job flags it as RUSTSEC-2026-0190; 1.0.103 is the patched release. A transitive dependency (via rolldown_error / vite_error), patch-compatible. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ebada86b89
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A stale pre-migrate lockfile can keep an optional-peer copy of vite-plus pinned to an older published version (e.g. a nested oxlint `vite-plus: '*'` peer pulled transitively by vite-plugin-checker/nuxt), which the `--no-frozen-lockfile` reinstall preserves rather than deduping, leaving `vp why` reporting two vite-plus versions. Remove the lockfiles and node_modules before migrate so the reinstall resolves that optional peer onto the in-tree managed version. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
- Node engines union floor gate: lift each disjunct of a multi-major pin independently instead of gating on the overall floor, so `^20.19.0 || ^22.0.0` lifts the below-floor 22 branch to `>=22.18.0 <23.0.0` (was left unchanged). - compat runner: resolve the rolldown-compat worker robustly across the source (sibling worker.js) and bundle (nested compat/worker.js) layouts. - yarn satisfaction: yarnrcSatisfiesVitePlus now requires the npmPreapprovedPackages vitest/@vitest exemptions, so an age-gated yarn project no longer takes the early "already using Vite+" path without them. - pnpm pending: detect missing minimumReleaseAgeExclude exemptions (shared PNPM_MINIMUM_RELEASE_AGE_EXCLUDES) in both detection and the bootstrap writer. - yarn hoisting: detect + apply the nmHoistingLimits opt-out on the bootstrap path, not only in rewriteMonorepoProject. - bun: gate the direct-vite injection to packages that need vitest/vite-plus/a browser provider instead of every workspace package. - typos: drop the `unparseable` whitelist; fix usages to `unparsable`. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
Replace the arity + element-wise comparison in supported_node_floor_range's union branch with a joined-string compare. It is equivalent (a dropped branch shortens the join, a lifted branch changes it) and reads clearer. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 595280825c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
On the existing-Vite+ upgrade path, reconcileVitePlusBootstrapPackage only removed the @vitest/* subset of REMOVE_PACKAGES from package.json, while the catalog rewrite deletes the full set (oxlint, oxlint-tsgolint, oxfmt, tsdown, @vitest/browser, @vitest/browser-preview). An existing project that declared one of the non-@vitest tools via `catalog:` was then left with a dangling catalog reference whose catalog entry had just been removed, so `pnpm install` aborted with ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC (delta-comic/oxlint-tsgolint, regle/tsdown). Iterate the full REMOVE_PACKAGES, matching the catalog removal and the fresh path (package-json.ts). Adds a unit test and a migration-upgrade-pnpm-bundled-catalog-dep snap proving the bundled tools are removed from both package.json and the catalog. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
- Node engines: bound the single-disjunct lift to its major, so `^20` / `20.18.0` become `>=20.19.0 <21.0.0` instead of an unbounded `>=20.19.0` that admitted unsupported Node 21/23. The single and union paths now share lift_disjunct; a genuinely open pin (`>=20`) stays open. - bun overrides: gate the bun catalog dependency resolver on supportCatalog in both the reconcile and the pending detection, so a standalone bun project's `overrides.vite: "catalog:"` is rewritten to the concrete core alias (and detection no longer reports it already settled) instead of leaving a catalog override that bun cannot resolve. - eslint versions import (false positive): the bundle resolves `../versions.js` correctly via the fix-versions-path tsdown plugin + a dist-root chunk; added a dist guard test that fails if any chunk's relative versions.js import stops resolving. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
Replace the hand-rolled recursive collectJsFiles walker with globSync, matching the in-package convention (utils/workspace.ts). Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
|
@codex review |
The .nvmrc to .node-version conversion only rewrote node-version-file
references in .github/workflows, leaving composite actions
(.github/actions/**/action.{yml,yaml}) pointing at the removed .nvmrc,
which breaks CI with "node version file ... does not exist".
Walk .github/actions recursively for action.{yml,yaml} alongside the
flat workflows scan and rewrite both.
Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
Replace the hand-rolled .github/workflows readdir and the .github/actions stack walk with a single globSync call, matching the in-package convention (utils/workspace.ts). nocase preserves the prior case-insensitive matching; the flat-workflows vs recursive-actions split is kept via two glob patterns. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fb2e405fcf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let major = floor.major; | ||
| let bracket = Range::parse(format!(">={major}.0.0 <{}.0.0", major + 1)).ok()?; | ||
| let supported_minimum = bracket.intersect(supported_range)?.min_version()?; |
There was a problem hiding this comment.
Handle Node ranges that span multiple majors
When engines.node or devEngines.runtime is a single range spanning multiple majors, this logic only considers the floor major. For a common pin such as >=18, bracket.intersect(supported_range) checks only 18.x, returns None, and the migration leaves the below-floor range in place, so pnpm can still skip the native binding; for >=20 <25 it rewrites to unbounded >=20.19.0, dropping the user's <25 cap. The disjunct needs to be intersected/split against the full supported range instead of classified solely from floor.major.
Useful? React with 👍 / 👎.
| for (const field of [ | ||
| 'devDependencies', | ||
| 'dependencies', | ||
| 'peerDependencies', | ||
| 'optionalDependencies', |
There was a problem hiding this comment.
Don't treat peer-only JS plugins as installed
When @oxlint/migrate emits a lint.jsPlugins entry for a package that is present only in peerDependencies, this collector marks it as available even though a package's own peers are not installed in its node_modules. The sanitizer then keeps the JS plugin reference, so vp lint can fail immediately with a plugin-load error after migration; this availability check should be limited to install groups such as dependencies/devDependencies/optionalDependencies.
Useful? React with 👍 / 👎.
| const resolver = readPnpmWorkspaceCatalogDependencyResolver(projectPath); | ||
| return ( | ||
| catalogVitePlusDependencyPending(pkg, resolver) || |
There was a problem hiding this comment.
Check workspace catalog refs before skipping bootstrap
In a pnpm monorepo with multiple named catalogs, this pending check only verifies the root package's vite-plus catalog reference. If the root uses an up-to-date default catalog but a workspace package depends on vite-plus: "catalog:legacy" whose catalog entry still points at an older Vite+ version, the dry-run above preserves that catalog spec and this root-only check returns false, so vp migrate can take the “already using Vite+” exit and leave that workspace on the stale local CLI. Scan all workspace package vite-plus catalog refs, not just the root manifest, before declaring the bootstrap satisfied.
Useful? React with 👍 / 👎.
| let escalate = resolve_local_vite_plus_version(project_path) | ||
| .is_some_and(|local| local_vite_plus_is_older(&local, env!("CARGO_PKG_VERSION"))); |
There was a problem hiding this comment.
Bypass broken local CLIs when migrating Node pins
When an existing Vite+ project has a local vite-plus at the same version as the global CLI, but its native optional binding was skipped because the project still declares a below-floor Node pin, this condition is false and migrate still delegates to the local JS entrypoint. That entrypoint imports ../binding/index.js at module load before migration/bin.js, so it throws Cannot find native binding before upgradeUnsupportedNodeVersions can repair the pin; the migrate path needs to fall back to the global CLI/runtime when the local binding is unavailable or a Node-pin repair is pending, not only when the local version is older.
Useful? React with 👍 / 👎.
RFC:
rfcs/migrate-existing-projects.mdProblem
Running
vp migrateon an existing v0.1.x Vite+ project did not upgrade cleanly: it delegated to the stale local CLI, leftpnpm-workspace.yamloverrides pinningvite/vitestto old versions, and skewed coverage providers. The v0.2.1 release notes currently tell users not to runvp migrateyet.What this does (verified)
vite-plusis older than the globalvp, run migrate from the global CLI.vite-plus/vite->core spec so the lockfile moves off 0.1.x."pnpm": {}misrouting that left stalepnpm-workspace.yamloverrides.vitestby usage: removed in the common case (vite-plus provides it transitively), kept + ecosystem-aligned when the project uses it directly or via a range-peer integration.@vitest/*ecosystem (coverage-v8/-istanbul, ui, web-worker) to the bundled version; exclude@vitest/eslint-plugin.Node.js version upgrade
A project pinning a Node version below the Vite+ supported range (
package.json#engines.node, currently^20.19.0 || ^22.18.0 || >=24.11.0) makes package managers skip the native binding's optional dependency ("Cannot find native binding"). Migration now reads the effective Node pin (.node-version->devEngines.runtime->engines.node, reusing the Rust runtime resolver, with.nvmrc/Volta converted to.node-versionfirst) and rewrites an exact ormajor.minorpin below the range to the concrete latest release of that major (24.3.0/24.2->24.18.0). A bare major or an open range that still resolves to a supported release is left unchanged. Interactive migration confirms the upgrade (default yes);--no-interactiveapplies it directly.vp migrateon a fresh project pinning.node-version= 24.3.0 (below the range).node-versionis rewritten24.3.0->24.18.0. The confirm prompt pauses the migration progress spinner so it does not animate beneath the prompt.Known gaps (draft, follow-ups)
apps/dashboard) is left without@vitest/browser-playwrightand a directvitest, so browser tests break. The fresh-migration path handles this; the upgrade path must too.vitestin the package that needs it instead of the shared root catalog.vitestpin into removal for official-@vitest/*-only projects.migration-*snap suite and do the docs /npm deprecaterollout, then drop the "do not runvp migrate" disclaimer.Manual pkg.pr.new migration testing
Use the repository helper to install an isolated pkg.pr.new global CLI and run the PR version of
vp migrateagainst any local project:The first argument accepts either a PR number or commit SHA. The helper keeps the normal
~/.vite-plusinstallation untouched, forces migration through the installed global preview CLI even when the project has a same-version local CLI, pinsvite-plusandvite/core to the matching pkg.pr.new URLs, refuses dirty Git worktrees by default, and forwards additional options such as--no-interactivetovp migrate.