When transitioning to Vite+, projects typically use standalone tools like vite, oxlint, oxfmt, and vitest, each with their own dependencies and configuration files. The vp migrate command automates the process of consolidating these tools into the unified Vite+ toolchain.
Problem: Manual migration is error-prone and time-consuming:
- Multiple dependency entries to update in package.json
- Various configuration files to merge (vite.config.ts, .oxlintrc, .oxfmtrc, etc.)
- Risk of missing configurations or incorrect merging
- Tedious process when migrating multiple packages in a monorepo
Solution: Automated migration using ast-grep for code transformation and brush-parser for shell script rewriting.
Related Commands:
vp create- Uses this same migration engine after generating code (see code-generator.md)vp migrate- This command, for migrating existing projects
- Dependency Consolidation: Replace standalone vite, vitest, oxlint, oxfmt dependencies with unified vite-plus
- Configuration Unification: Merge .oxlintrc, .oxfmtrc into vite.config.ts
- Safe: Preview changes before applying
- Intelligent: Preserve custom configurations and user overrides
- Monorepo-Aware: Migrate multiple packages efficiently
What this command migrates:
- ✅ Dependencies: vite, vitest, oxlint, oxfmt → vite-plus
- ✅ Overrides: Force vite → vite-plus (for all dependencies)
- npm/pnpm/bun: Adds
overrides.vitemapping - yarn: Adds
resolutions.vitemapping - Benefit: Code keeps
import from 'vite'- automatically resolves to vite-plus
- npm/pnpm/bun: Adds
- ✅ Configuration files:
- .oxlintrc → vite.config.ts (lint section)
- .oxfmtrc → vite.config.ts (format section)
What this command optionally migrates (prompted):
- ✅ Git hooks: husky + lint-staged →
vp config+vp staged- Rewrites
prepare: "husky"→prepare: "vp config" - Migrates lint-staged config into
stagedin vite.config.ts - Replaces
.husky/pre-commitwith.vite-hooks/pre-commitusingvp staged - Removes
huskyandlint-stagedfrom devDependencies
- Rewrites
- ✅ ESLint → oxlint (via
@oxlint/migrate): converts ESLint flat config to.oxlintrc.json, which is then merged intovite.config.tsby the existing flow - ✅ Prettier → oxfmt (via
vp fmt --migrate=prettier): converts Prettier config to.oxfmtrc.json, which is then merged intovite.config.tsby the existing flow
What this command does NOT migrate:
- ❌ Package.json scripts → vite-task.json (different feature)
- ❌ TypeScript configuration changes
- ❌ Build tool changes (webpack/rollup → vite)
These are consolidation migrations, not feature migrations.
When a project already has vite-plus in its dependencies, vp migrate skips the full dependency/config migration and only runs remaining partial migrations:
- ESLint → Oxlint: If
eslintis still present with a flat config, offers ESLint migration - Prettier → Oxfmt: If
prettieris still present with a config file, offers Prettier migration - Git hooks: If
huskyand/orlint-stagedare still present, offers hooks migration
All checks run independently — a project may need one, some, or none.
vp migrateThe migration uses a two-phase architecture: all user prompts are collected upfront (Phase 1), then all work is executed without interruption (Phase 2). This lets the user see the full picture before any changes begin.
All prompts are presented sequentially before any work begins:
- Confirm migration: "Migrate this project to Vite+?"
- Package manager: Select or auto-detect (pnpm/npm/yarn)
- Pre-commit hooks: "Set up pre-commit hooks?" + preflight validation (read-only check for git root, existing hook tools)
- Agent selection: "Which agents are you using?" (multiselect)
- Agent file conflicts: Per existing file — "Agent instructions already exist at X. Append or Skip?" (only for files without auto-update markers)
- Editor selection: "Which editor are you using?"
- Editor file conflicts: Per existing file — "X already exists. Merge or Skip?"
- ESLint migration: If ESLint config detected — "Migrate ESLint rules to Oxlint?"
- Prettier migration: If Prettier config detected — "Migrate Prettier to Oxfmt?"
- Migration plan summary: Display all planned actions before execution
In non-interactive mode (--no-interactive), Phase 1 uses defaults (no prompts shown, no summary displayed).
$ vp migrate
VITE+ - The Unified Toolchain for the Web
◆ Migrate this project to Vite+?
│ Yes
◆ Which package manager would you like to use?
│ pnpm (recommended)
◆ Set up pre-commit hooks?
│ Yes
◆ Which agents are you using?
│ Claude Code
◆ CLAUDE.md already exists.
│ Append
◆ Which editor are you using?
│ VSCode
◆ .vscode/settings.json already exists.
│ Merge
◆ Migrate ESLint rules to Oxlint using @oxlint/migrate?
│ Yes
◆ Migrate Prettier to Oxfmt?
│ Yes
Migration plan:
- Install pnpm and dependencies
- Rewrite configs and dependencies for Vite+
- Migrate ESLint rules to Oxlint
- Migrate Prettier to Oxfmt
- Set up pre-commit hooks
- Write agent instructions (CLAUDE.md, append)
- Write editor config (.vscode/, merge)All work runs sequentially with spinner feedback — no further user interaction:
- Download package manager + version validation
- Upgrade yarn if needed (yarn <4.10.0)
- Run
vp installto prepare dependencies - Check vite/vitest versions (abort if unsupported)
- Migrate ESLint → Oxlint (if approved in Phase 1, via
@oxlint/migrate) 5b. Migrate Prettier → Oxfmt (if approved in Phase 1, viavp fmt --migrate=prettier) - Rewrite configs (dependencies, overrides, config file merging)
- Install git hooks (if approved)
- Write agent instructions (using pre-resolved conflict decisions)
- Write editor configs (using pre-resolved conflict decisions)
- Reinstall dependencies (final
vp install)
pnpm@latest installing...
pnpm@<semver> installed
Migrating ESLint config to Oxlint...
ESLint config migrated to .oxlintrc.json
Replacing ESLint comments with Oxlint equivalents...
ESLint comments replaced
✔ Removed eslint.config.mjs
✔ Created vite.config.ts in vite.config.ts
✔ Merged .oxlintrc.json into vite.config.ts
✔ Merged staged config into vite.config.ts
Wrote agent instructions to AGENTS.md
✔ Migration completed!Before:
{
"name": "my-package",
"dependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"vite": "^8.0.0",
"vitest": "^4.0.0",
"oxlint": "^0.1.0",
"oxfmt": "^0.1.0",
"@vitest/browser": "^4.0.0",
"@vitest/browser-playwright": "^4.0.0",
"@vitejs/plugin-react": "^4.2.0"
}
}After:
{
"name": "my-package",
"dependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
"vitest": "npm:@voidzero-dev/vite-plus-test@latest",
"@vitejs/plugin-react": "^4.2.0"
},
"overrides": {
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
}
}Important:
overrides.viteensures any dependency requiringvitegetsvite-plusinstead- rewrite
import from 'vite'toimport from 'vite-plus' - rewrite
import from 'vite/{name}'toimport from 'vite-plus/{name}', e.g.:import from 'vite/module-runner'toimport from 'vite-plus/module-runner' - rewrite
import from 'vitest'toimport from 'vite-plus/test' - rewrite
import from 'vitest/config'toimport from 'vite-plus' - rewrite
import from 'vitest/{name}'toimport from 'vite-plus/test/{name}', e.g.:import from 'vitest/node'toimport from 'vite-plus/test/node' - rewrite
import from '@vitest/browser'toimport from 'vite-plus/test/browser' - rewrite
import from '@vitest/browser/{name}'toimport from 'vite-plus/test/browser/{name}', e.g.:import from '@vitest/browser/context'toimport from 'vite-plus/test/browser/context' - rewrite
import from '@vitest/browser-playwright'toimport from 'vite-plus/test/browser-playwright' - rewrite
import from '@vitest/browser-playwright/{name}'toimport from 'vite-plus/test/browser-playwright/{name}'
Note: For Yarn, use resolutions instead of overrides.
Before (.oxlintrc):
{
"rules": {
"no-unused-vars": "error",
"no-console": "warn"
},
"ignorePatterns": ["dist", "node_modules"]
}After (merged into vite.config.ts):
import { defineConfig } from 'vite-plus';
export default defineConfig({
plugins: [],
// Oxlint configuration
lint: {
options: {
typeAware: true,
typeCheck: true,
},
rules: {
'no-unused-vars': 'error',
'no-console': 'warn',
},
ignorePatterns: ['dist', 'node_modules'],
},
});Note: If
tsconfig.jsoncontainscompilerOptions.baseUrl,typeAwareandtypeCheckare not injected because oxlint's TypeScript checker does not yet supportbaseUrl. Runnpx @andrewbranch/ts5to6 --fixBaseUrl .to migrate away frombaseUrl.
Before (.oxfmtrc):
{
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
}After (merged into vite.config.ts):
import { defineConfig } from 'vite-plus';
export default defineConfig({
plugins: [],
// Oxfmt configuration
fmt: {
printWidth: 100,
tabWidth: 2,
semi: true,
singleQuote: true,
trailingComma: 'es5',
},
});effect files:
- vitest.config.ts
- vite.config.ts
Before (import from 'vitest/config'):
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
},
});After (import from 'vite-plus'):
import { defineConfig } from 'vite-plus';
export default defineConfig({
test: {
globals: true,
},
});Before (import from 'vite'):
import { defineConfig } from 'vite';
export default defineConfig({
test: {
globals: true,
},
});After (import from 'vite-plus'):
import { defineConfig } from 'vite-plus';
export default defineConfig({
test: {
globals: true,
},
});Before:
my-package/
├── package.json # Has vite, vitest, oxlint, oxfmt
├── vite.config.ts # Vite config
├── vitest.config.ts # Vitest config
├── .oxlintrc # Oxlint config
├── .oxfmtrc # Oxfmt config
└── src/
After:
my-package/
├── package.json # Only has vite-plus
├── vitest.config.ts # Vitest config
├── vite.config.ts # Unified config (all merged)
└── src/
vite.config.ts (after migration):
// Import from 'vite' still works - overrides maps it to vite-plus
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite-plus';
export default defineConfig({
// Vite configuration
plugins: [react()],
server: {
port: 3000,
},
build: {
target: 'esnext',
},
// lint configuration (merged from .oxlintrc)
lint: {
rules: {
'no-unused-vars': 'error',
'no-console': 'warn',
},
ignorePatterns: ['dist', 'node_modules'],
},
// format configuration (merged from .oxfmtrc)
fmt: {
printWidth: 100,
tabWidth: 2,
semi: true,
singleQuote: true,
trailingComma: 'es5',
},
});vitest.config.ts (after migration):
import { defineConfig } from 'vite-plus';
export default defineConfig({
test: {
globals: true,
},
});pnpm-workspace.yaml
catalog:
vite: npm:@voidzero-dev/vite-plus-core@latest
vitest: npm:@voidzero-dev/vite-plus-test@latest
overrides:
vite: 'catalog:'
vitest: 'catalog:'
peerDependencyRules:
allowAny:
- vite
- vitest
allowedVersions:
vite: '*'
vitest: '*'package.json
{
"devDependencies": {
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
},
"overrides": {
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
}
}.yarnrc.yml
catalog:
vite: npm:@voidzero-dev/vite-plus-core@latest
vitest: npm:@voidzero-dev/vite-plus-test@latestpackage.json
{
"resolutions": {
"vite": "catalog:",
"vitest": "catalog:"
}
}TODO: Add support for yarn v1
A successful migration should:
- ✅ Replace all standalone tool dependencies with vite-plus
- ✅ Add package.json overrides to force vite → vite-plus (for transitive deps)
- ✅ Transform vitest imports to vite/test (since vitest is removed)
- ✅ Merge all configurations into vite.config.ts
- ✅ Preserve all user customizations and settings
- ✅ Remove redundant configuration files
- ✅ Provide clear feedback and next steps
- ✅ Handle monorepo migrations efficiently
- ✅ Be safe and transparent about what changes
When an ESLint flat config (eslint.config.{js,mjs,cjs,ts,mts,cts}) and eslint dependency are detected, vp migrate offers to convert the ESLint configuration to oxlint using @oxlint/migrate.
Flow: ESLint → oxlint (via @oxlint/migrate) → vite+ (existing merge flow)
Steps:
- Run
vpx @oxlint/migrate --merge --type-aware --with-nursery --detailsto generate.oxlintrc.json - Run
vpx @oxlint/migrate --replace-eslint-commentsto replaceeslint-disablecomments - Delete the ESLint config file
- Remove
eslintfromdevDependencies - Rewrite
eslintscripts inpackage.jsontovp lint, stripping ESLint-only flags - Rewrite
eslintreferences in lint-staged configs (package.jsonlint-stagedfield and standalone config files like.lintstagedrc.json) - The existing migration flow picks up
.oxlintrc.jsonand merges it intovite.config.ts
Script Rewriting (powered by brush-parser for shell AST parsing):
| Before | After |
|---|---|
eslint . |
vp lint . |
eslint --cache --ext .ts --fix . |
vp lint --fix . |
NODE_ENV=test eslint --cache . |
NODE_ENV=test vp lint . |
cross-env NODE_ENV=test eslint --cache . |
cross-env NODE_ENV=test vp lint . |
eslint . && vite build |
vp lint . && vite build |
if [ -f .eslintrc ]; then eslint .; fi |
if [ -f .eslintrc ]; then vp lint . fi |
npx eslint . |
npx eslint . (npx/bunx wrappers preserved) |
Stripped ESLint-only flags: --cache, --ext, --parser, --parser-options, --plugin, --rulesdir, --resolve-plugins-relative-to, --output-file, --env, --no-eslintrc, --no-error-on-unmatched-pattern, --debug, --no-inline-config
The rewriter handles:
- Compound commands:
&&,||,|,if/then/fi,while/do/done,for,case, brace groups{ ...; }, subshells(...) - Environment variable prefixes:
NODE_ENV=test eslint . - cross-env wrappers:
cross-env NODE_ENV=test eslint . - No-op safety: Scripts without
eslintare returned unchanged (no formatting corruption from AST round-tripping)
Legacy ESLint Config Handling:
If only a legacy ESLint config (.eslintrc*) is detected without a flat config (eslint.config.*), the migration warns and skips ESLint migration. The warning guides users to upgrade to ESLint v9 first, since @oxlint/migrate only supports flat configs:
Legacy ESLint configuration detected (.eslintrc). Automatic migration to Oxlint requires ESLint v9+ with flat config format (eslint.config.*). Please upgrade to ESLint v9 first: https://eslint.org/docs/latest/use/migrate-to-9.0.0
Behavior:
- Interactive mode: prompts user for confirmation upfront (Phase 1), executes later (Phase 2)
- Non-interactive mode: auto-runs without prompting
- Failure is non-blocking — warns and continues with the rest of migration
- Re-runnable: if user declines initially, running
vp migrateagain offers eslint migration
When a Prettier configuration file (.prettierrc*, prettier.config.*, or "prettier" key in package.json) and prettier dependency are detected, vp migrate offers to convert the Prettier configuration to oxfmt using vp fmt --migrate=prettier.
Flow: Prettier → oxfmt (via vp fmt --migrate=prettier) → vite+ (existing merge flow)
Steps:
- Run
vp fmt --migrate=prettierto generate.oxfmtrc.jsonfrom Prettier config (if a standalone config file exists, notpackage.json#prettier) - Delete all Prettier config files (
.prettierrc*,prettier.config.*) - Remove
"prettier"key from package.json if present - Remove
prettierandprettier-plugin-*fromdevDependencies/dependencies - Rewrite
prettierscripts inpackage.jsontovp fmt, stripping Prettier-only flags - Rewrite
prettierreferences in lint-staged configs - Warn about
.prettierignoreif present (Oxfmt supports it, butignorePatternsis recommended) - The existing migration flow picks up
.oxfmtrc.jsonand merges it intovite.config.ts
Script Rewriting (powered by brush-parser for shell AST parsing):
| Before | After |
|---|---|
prettier . |
vp fmt . |
prettier --write . |
vp fmt . |
prettier --check . |
vp fmt --check . |
prettier --list-different . |
vp fmt --check . |
prettier -l . |
vp fmt --check . |
prettier --write --single-quote --tab-width 4 . |
vp fmt . |
prettier --config .prettierrc --write . |
vp fmt . |
prettier --plugin prettier-plugin-tailwindcss . |
vp fmt . |
cross-env NODE_ENV=test prettier --write . |
cross-env NODE_ENV=test vp fmt . |
prettier --write . && eslint --fix . |
vp fmt . && eslint --fix . |
npx prettier --write . |
npx prettier --write . (npx/bunx wrappers preserved) |
Stripped Prettier-only flags:
- Value flags:
--config,--ignore-path,--plugin,--parser,--cache-location,--cache-strategy,--log-level,--stdin-filepath,--cursor-offset,--range-start,--range-end,--config-precedence,--tab-width,--print-width,--trailing-comma,--arrow-parens,--prose-wrap,--end-of-line,--html-whitespace-sensitivity,--quote-props,--embedded-language-formatting,--experimental-ternaries - Boolean flags:
--write,--cache,--no-config,--no-editorconfig,--with-node-modules,--require-pragma,--insert-pragma,--no-bracket-spacing,--single-quote,--no-semi,--jsx-single-quote,--bracket-same-line,--use-tabs,--debug-check,--debug-print-doc,--debug-benchmark,--debug-repeat
Converted flags: --list-different / -l → --check
Kept flags: --check, --fix, --no-error-on-unmatched-pattern, positional args (file paths/globs)
Behavior:
- Interactive mode: prompts user for confirmation upfront (Phase 1), executes later (Phase 2)
- Non-interactive mode: auto-runs without prompting
- Failure is non-blocking — warns and continues with the rest of migration
- Re-runnable: if user declines initially, running
vp migrateagain offers prettier migration
- ast-grep - Structural search and replace tool
- Turborepo Codemods - Similar migration approach
- jscodeshift - Alternative AST transformation tool
- @ast-grep/napi - Node.js bindings for ast-grep
- @oxlint/migrate - ESLint to oxlint migration tool
- brush-parser - Shell AST parser for script rewriting (Rust)
- @clack/prompts - Beautiful CLI prompts
- typescript - For parsing TypeScript configs
- Vue 2 to Vue 3 Migration - Similar migration tool
- React Codemod - React migration scripts
- Angular Update Guide - Automated Angular migrations