Problem
vp migrate rewrites every vite/vitest import specifier to vite-plus, then tries to exclude the cases where that's wrong with package-level heuristics (vite-plugin-*/unplugin-* names, vite in dependencies/peerDependencies). This is the wrong altitude: the correctness of the rewrite is a property of the imported symbol, not of the package, so the heuristics leak.
vite-plus is not a superset of the Vite library. It is Vite (export * from '@voidzero-dev/vite-plus-core' in packages/cli/src/index.ts) plus a small set of deliberately-owned overrides (defineConfig, defineProject, lazyPlugins). Those are the only symbols where importing from vite-plus is better than importing from vite. Rewriting any other (pass-through) symbol carries no benefit and can break, because vite-plus's exposed surface is not guaranteed to match vite's.
Reproduction
A monorepo package that uses Vite's programmatic API. In packages/cloudflare/src/deploy.ts:
// before migrate (correct):
type ProjectViteApi = Pick<typeof import("vite"), "createBuilder" | "loadConfigFromFile">;
// after migrate (broken):
type ProjectViteApi = Pick<typeof import("vite-plus"), "createBuilder" | "loadConfigFromFile">;
createBuilder/loadConfigFromFile are Vite-core APIs; vite resolves to vite-plus-core and has them, but typeof import("vite-plus") does not expose them, so the type breaks. Real-world instance: fengmk2/vinext#22 (comment)
Note: this is not a resolution problem. packages/cloudflare does depend on vite-plus and it resolves fine. It is purely a surface mismatch.
Root cause
crates/vite_migration/src/import_rewriter.rs (get_package_rewrite_context / SkipPackages) decides whether to rewrite a package's vite imports from dependencies/peerDependencies + the plugin-name convention. A package that declares vite only in devDependencies and isn't a plugin (like packages/cloudflare) is not skipped, so its programmatic/type vite imports get rewritten.
Proposed fix
Make the rewrite symbol-scoped. Rewrite a vite/vitest import to vite-plus only for the bindings vite-plus deliberately owns; leave every other binding on vite/vitest, splitting a mixed import when needed:
import { defineConfig } from 'vite' becomes from 'vite-plus'
import { createBuilder, loadConfigFromFile } from 'vite' stays unchanged
import { defineConfig, loadEnv } from 'vite' splits: defineConfig from vite-plus, loadEnv from vite
typeof import("vite") used for createBuilder stays unchanged
Derive the owned-symbol set from vite-plus's own explicit (non-star) exports so it stays in sync. This is a smaller migrator: it removes the plugin-name check and the deps/peerDeps skip (one correct invariant replaces the topology guesses) and fixes the whole class at once (programmatic API, type-only imports, plugin internals, monorepo consumers).
Notes
- Interim option if symbol-splitting is too much for a first pass: scope the rewrite to config entry files only (
vite.config.*, vitest.config.*), where the unified defineConfig is the only real reason to rewrite. Simpler, still fixes this case.
- Possible separate bug: if
export * is meant to forward createBuilder and the built vite-plus .d.ts drops it, that is a vite-plus types bug worth fixing independently. The migrate should not depend on vite-plus being a perfect superset regardless.
Problem
vp migraterewrites everyvite/vitestimport specifier tovite-plus, then tries to exclude the cases where that's wrong with package-level heuristics (vite-plugin-*/unplugin-*names,viteindependencies/peerDependencies). This is the wrong altitude: the correctness of the rewrite is a property of the imported symbol, not of the package, so the heuristics leak.vite-plusis not a superset of the Vite library. It is Vite (export * from '@voidzero-dev/vite-plus-core'inpackages/cli/src/index.ts) plus a small set of deliberately-owned overrides (defineConfig,defineProject,lazyPlugins). Those are the only symbols where importing fromvite-plusis better than importing fromvite. Rewriting any other (pass-through) symbol carries no benefit and can break, becausevite-plus's exposed surface is not guaranteed to matchvite's.Reproduction
A monorepo package that uses Vite's programmatic API. In
packages/cloudflare/src/deploy.ts:createBuilder/loadConfigFromFileare Vite-core APIs;viteresolves tovite-plus-coreand has them, buttypeof import("vite-plus")does not expose them, so the type breaks. Real-world instance: fengmk2/vinext#22 (comment)Note: this is not a resolution problem.
packages/cloudflaredoes depend onvite-plusand it resolves fine. It is purely a surface mismatch.Root cause
crates/vite_migration/src/import_rewriter.rs(get_package_rewrite_context/SkipPackages) decides whether to rewrite a package'sviteimports fromdependencies/peerDependencies+ the plugin-name convention. A package that declaresviteonly indevDependenciesand isn't a plugin (likepackages/cloudflare) is not skipped, so its programmatic/typeviteimports get rewritten.Proposed fix
Make the rewrite symbol-scoped. Rewrite a
vite/vitestimport tovite-plusonly for the bindingsvite-plusdeliberately owns; leave every other binding onvite/vitest, splitting a mixed import when needed:import { defineConfig } from 'vite'becomesfrom 'vite-plus'import { createBuilder, loadConfigFromFile } from 'vite'stays unchangedimport { defineConfig, loadEnv } from 'vite'splits:defineConfigfromvite-plus,loadEnvfromvitetypeof import("vite")used forcreateBuilderstays unchangedDerive the owned-symbol set from
vite-plus's own explicit (non-star) exports so it stays in sync. This is a smaller migrator: it removes the plugin-name check and the deps/peerDeps skip (one correct invariant replaces the topology guesses) and fixes the whole class at once (programmatic API, type-only imports, plugin internals, monorepo consumers).Notes
vite.config.*,vitest.config.*), where the unifieddefineConfigis the only real reason to rewrite. Simpler, still fixes this case.export *is meant to forwardcreateBuilderand the builtvite-plus.d.tsdrops it, that is a vite-plus types bug worth fixing independently. The migrate should not depend on vite-plus being a perfect superset regardless.