Skip to content

vp migrate: only rewrite the vite/vitest imports that vite-plus actually owns #2004

Description

@fengmk2

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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions