Repository navigation
vp migrate: only rewrite the vite/vitest imports that vite-plus actually owns #2004
Description
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'becomesfrom 'vite-plus'import { createBuilder, loadConfigFromFile } from 'vite'stays unchangedimport { defineConfig, loadEnv } from 'vite'splits:defineConfigfromvite-plus,loadEnvfromvitetypeof import("vite")used forcreateBuilderstays 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 unifieddefineConfigis the only real reason to rewrite. Simpler, still fixes this case. - Possible separate bug: if
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.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
feat(migrate): scope the vite to vite-plus rewrite to config entry files
refactor(migrate): drop dead extra_config_files path, preserve tsconf…
style(migrate): apply rustfmt to the #2004 import_rewriter tests
fix(migrate): apply 13 code-review fixes across migrate paths
test(migrate): declare module 'vite' augmentations follow the config …
fix(migrate): keep declare module 'vite' augmentations on 'vite'