From 6741f2c473e77027320036f6615f0fc76bb36e5c Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 21 Apr 2026 22:58:19 +0800 Subject: [PATCH 01/11] feat(cli/create): auto-migrate ESLint/Prettier to oxlint/oxfmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vite+ is opinionated about oxlint + oxfmt, but `vp create` previously scaffolded templates (e.g. `create-vite --template react-ts`) and left their ESLint flat config and Prettier configs untouched — forcing users to run `vp migrate` as a second step. This wires the existing migration helpers into `vp create` so freshly scaffolded projects are already on the unified toolchain, with no confirmation prompt. - Relocate the ESLint/Prettier prompt/warn/confirm helpers from migration/bin.ts to migration/migrator.ts so create/bin.ts can reuse them without triggering bin.ts's top-level `main()` side effect. - In create/bin.ts, run `promptEslintMigration` and `promptPrettierMigration` after `runViteInstall` (so `@oxlint/migrate` can resolve the template's ESLint plugin imports) and before `runViteFmt`. Always non-interactive — unlike `vp migrate`, the scaffold has no prior user preferences to respect. - Gate on `installSummary.status === 'installed'` so VP_SKIP_INSTALL snap-test runs skip migration cleanly. - Update rfcs/code-generator.md: flip the "does NOT migrate ESLint/Prettier" bullets, update flow diagrams and example output, and document why create skips the confirmation prompt. --- packages/cli/src/create/bin.ts | 28 ++++++ packages/cli/src/migration/bin.ts | 130 ++----------------------- packages/cli/src/migration/migrator.ts | 126 +++++++++++++++++++++++- rfcs/code-generator.md | 54 +++++++--- 4 files changed, 201 insertions(+), 137 deletions(-) diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index cedf7fdf5e..644239d4a2 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -10,6 +10,8 @@ import { detectFramework, hasFrameworkShim, installGitHooks, + promptEslintMigration, + promptPrettierMigration, rewriteMonorepo, rewriteMonorepoProject, rewriteStandaloneProject, @@ -969,6 +971,19 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h installSummary = await runViteInstall(workspaceInfo.rootDir, options.interactive, installArgs, { silent: compactOutput, }); + // Auto-migrate ESLint → Oxlint and Prettier → Oxfmt after install so + // `@oxlint/migrate` can resolve the template's ESLint plugin packages. + // Runs unconditionally (Vite+ is opinionated about oxlint/oxfmt — + // freshly scaffolded projects should already be on the unified toolchain). + // Gated on install success — skipping when install was bypassed (e.g. + // VP_SKIP_INSTALL in snap tests) avoids failing module resolution. + if (installSummary.status === 'installed') { + updateCreateProgress('Migrating lint and format tools'); + pauseCreateProgress(); + await promptEslintMigration(fullPath, /* interactive */ false); + await promptPrettierMigration(fullPath, /* interactive */ false); + resumeCreateProgress(); + } updateCreateProgress('Formatting code'); await runViteFmt(workspaceInfo.rootDir, options.interactive, [projectDir], { silent: compactOutput, @@ -988,6 +1003,19 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { silent: compactOutput, }); + // Auto-migrate ESLint → Oxlint and Prettier → Oxfmt after install so + // `@oxlint/migrate` can resolve the template's ESLint plugin packages. + // Runs unconditionally (Vite+ is opinionated about oxlint/oxfmt — + // freshly scaffolded projects should already be on the unified toolchain). + // Gated on install success — skipping when install was bypassed (e.g. + // VP_SKIP_INSTALL in snap tests) avoids failing module resolution. + if (installSummary.status === 'installed') { + updateCreateProgress('Migrating lint and format tools'); + pauseCreateProgress(); + await promptEslintMigration(fullPath, /* interactive */ false); + await promptPrettierMigration(fullPath, /* interactive */ false); + resumeCreateProgress(); + } updateCreateProgress('Formatting code'); await runViteFmt(fullPath, options.interactive, undefined, { silent: compactOutput }); } diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index 11eb95eb89..e8a13ba432 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -43,6 +43,8 @@ import { addFrameworkShim, checkVitestVersion, checkViteVersion, + confirmEslintMigration, + confirmPrettierMigration, detectEslintProject, detectFramework, detectNodeVersionManagerFile, @@ -54,136 +56,18 @@ import { migrateNodeVersionManagerFile, migratePrettierToOxfmt, preflightGitHooksSetup, + promptEslintMigration, + promptPrettierMigration, rewriteMonorepo, rewriteStandaloneProject, + warnLegacyEslintConfig, + warnPackageLevelEslint, + warnPackageLevelPrettier, type Framework, type NodeVersionManagerDetection, } from './migrator.ts'; import { createMigrationReport, type MigrationReport } from './report.ts'; -function warnPackageLevelEslint() { - prompts.log.warn( - 'ESLint detected in workspace packages but no root config found. Package-level ESLint must be migrated manually.', - ); -} - -function warnLegacyEslintConfig(legacyConfigFile: string) { - prompts.log.warn( - `Legacy ESLint configuration detected (${legacyConfigFile}). ` + - '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', - ); -} - -async function confirmEslintMigration(interactive: boolean): Promise { - if (interactive) { - const confirmed = await prompts.confirm({ - message: - 'Migrate ESLint rules to Oxlint using @oxlint/migrate?\n ' + - styleText( - 'gray', - "Oxlint is Vite+'s built-in linter — significantly faster than ESLint with compatible rule support. @oxlint/migrate converts your existing rules automatically.", - ), - initialValue: true, - }); - if (prompts.isCancel(confirmed)) { - cancelAndExit(); - } - return confirmed; - } - return true; -} - -async function promptEslintMigration( - projectPath: string, - interactive: boolean, - packages?: WorkspacePackage[], -): Promise { - const eslintProject = detectEslintProject(projectPath, packages); - if (eslintProject.hasDependency && !eslintProject.configFile && eslintProject.legacyConfigFile) { - warnLegacyEslintConfig(eslintProject.legacyConfigFile); - return false; - } - if (!eslintProject.hasDependency) { - return false; - } - if (!eslintProject.configFile) { - // Packages have eslint but no root config → warn and skip - warnPackageLevelEslint(); - return false; - } - const confirmed = await confirmEslintMigration(interactive); - if (!confirmed) { - return false; - } - const ok = await migrateEslintToOxlint( - projectPath, - interactive, - eslintProject.configFile, - packages, - ); - if (!ok) { - cancelAndExit('ESLint migration failed. Fix the issue and re-run `vp migrate`.', 1); - } - return true; -} - -function warnPackageLevelPrettier() { - prompts.log.warn( - 'Prettier detected in workspace packages but no root config found. Package-level Prettier must be migrated manually.', - ); -} - -async function confirmPrettierMigration(interactive: boolean): Promise { - if (interactive) { - const confirmed = await prompts.confirm({ - message: - 'Migrate Prettier to Oxfmt?\n ' + - styleText( - 'gray', - "Oxfmt is Vite+'s built-in formatter that replaces Prettier with faster performance. Your configuration will be converted automatically.", - ), - initialValue: true, - }); - if (prompts.isCancel(confirmed)) { - cancelAndExit(); - } - return confirmed; - } - prompts.log.info('Prettier configuration detected. Auto-migrating to Oxfmt...'); - return true; -} - -async function promptPrettierMigration( - projectPath: string, - interactive: boolean, - packages?: WorkspacePackage[], -): Promise { - const prettierProject = detectPrettierProject(projectPath, packages); - if (!prettierProject.hasDependency) { - return false; - } - if (!prettierProject.configFile) { - // Packages have prettier but no root config → warn and skip - warnPackageLevelPrettier(); - return false; - } - const confirmed = await confirmPrettierMigration(interactive); - if (!confirmed) { - return false; - } - const ok = await migratePrettierToOxfmt( - projectPath, - interactive, - prettierProject.configFile, - packages, - ); - if (!ok) { - cancelAndExit('Prettier migration failed. Fix the issue and re-run `vp migrate`.', 1); - } - return true; -} - async function confirmNodeVersionFileMigration( interactive: boolean, detection: NodeVersionManagerDetection, diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts index 722e7b231e..6afc67aa13 100644 --- a/packages/cli/src/migration/migrator.ts +++ b/packages/cli/src/migration/migrator.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { styleText } from 'node:util'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; import spawn from 'cross-spawn'; @@ -27,7 +28,7 @@ import { import { editJsonFile, isJsonFile, readJsonFile } from '../utils/json.ts'; import { detectPackageMetadata } from '../utils/package.ts'; import { displayRelative, rulesDir } from '../utils/path.ts'; -import { getSpinner } from '../utils/prompts.ts'; +import { cancelAndExit, getSpinner } from '../utils/prompts.ts'; import { findTsconfigFiles, hasBaseUrlInTsconfig, @@ -2524,3 +2525,126 @@ export function migrateNodeVersionManagerFile( } return true; } + +export function warnPackageLevelEslint() { + prompts.log.warn( + 'ESLint detected in workspace packages but no root config found. Package-level ESLint must be migrated manually.', + ); +} + +export function warnLegacyEslintConfig(legacyConfigFile: string) { + prompts.log.warn( + `Legacy ESLint configuration detected (${legacyConfigFile}). ` + + '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', + ); +} + +export async function confirmEslintMigration(interactive: boolean): Promise { + if (interactive) { + const confirmed = await prompts.confirm({ + message: + 'Migrate ESLint rules to Oxlint using @oxlint/migrate?\n ' + + styleText( + 'gray', + "Oxlint is Vite+'s built-in linter — significantly faster than ESLint with compatible rule support. @oxlint/migrate converts your existing rules automatically.", + ), + initialValue: true, + }); + if (prompts.isCancel(confirmed)) { + cancelAndExit(); + } + return confirmed; + } + return true; +} + +export async function promptEslintMigration( + projectPath: string, + interactive: boolean, + packages?: WorkspacePackage[], +): Promise { + const eslintProject = detectEslintProject(projectPath, packages); + if (eslintProject.hasDependency && !eslintProject.configFile && eslintProject.legacyConfigFile) { + warnLegacyEslintConfig(eslintProject.legacyConfigFile); + return false; + } + if (!eslintProject.hasDependency) { + return false; + } + if (!eslintProject.configFile) { + // Packages have eslint but no root config → warn and skip + warnPackageLevelEslint(); + return false; + } + const confirmed = await confirmEslintMigration(interactive); + if (!confirmed) { + return false; + } + const ok = await migrateEslintToOxlint( + projectPath, + interactive, + eslintProject.configFile, + packages, + ); + if (!ok) { + cancelAndExit('ESLint migration failed. Fix the issue and re-run `vp migrate`.', 1); + } + return true; +} + +export function warnPackageLevelPrettier() { + prompts.log.warn( + 'Prettier detected in workspace packages but no root config found. Package-level Prettier must be migrated manually.', + ); +} + +export async function confirmPrettierMigration(interactive: boolean): Promise { + if (interactive) { + const confirmed = await prompts.confirm({ + message: + 'Migrate Prettier to Oxfmt?\n ' + + styleText( + 'gray', + "Oxfmt is Vite+'s built-in formatter that replaces Prettier with faster performance. Your configuration will be converted automatically.", + ), + initialValue: true, + }); + if (prompts.isCancel(confirmed)) { + cancelAndExit(); + } + return confirmed; + } + prompts.log.info('Prettier configuration detected. Auto-migrating to Oxfmt...'); + return true; +} + +export async function promptPrettierMigration( + projectPath: string, + interactive: boolean, + packages?: WorkspacePackage[], +): Promise { + const prettierProject = detectPrettierProject(projectPath, packages); + if (!prettierProject.hasDependency) { + return false; + } + if (!prettierProject.configFile) { + // Packages have prettier but no root config → warn and skip + warnPackageLevelPrettier(); + return false; + } + const confirmed = await confirmPrettierMigration(interactive); + if (!confirmed) { + return false; + } + const ok = await migratePrettierToOxfmt( + projectPath, + interactive, + prettierProject.configFile, + packages, + ); + if (!ok) { + cancelAndExit('Prettier migration failed. Fix the issue and re-run `vp migrate`.', 1); + } + return true; +} diff --git a/rfcs/code-generator.md b/rfcs/code-generator.md index 5c811f3344..69c1314eb2 100644 --- a/rfcs/code-generator.md +++ b/rfcs/code-generator.md @@ -87,12 +87,16 @@ ANY template (bingo or universal) ↓ Template generates code ↓ -Vite+ auto-detects vite-related tools: +Vite+ auto-detects vite-related tools + lint/format tools: • Standalone vite, vitest, oxlint, oxfmt + • ESLint (flat config) and Prettier ↓ Auto-migrate to unified vite-plus: • Dependencies: vite + vitest + oxlint + oxfmt → vite-plus • Configs: Merge vitest.config.ts, .oxlintrc, .oxfmtrc → vite.config.ts + • ESLint → oxlint (via @oxlint/migrate) + • Prettier → oxfmt + • Rewrite lint-staged entries to vp lint / vp fmt ↓ Monorepo integration: • Prompt for workspace dependencies @@ -103,7 +107,8 @@ Monorepo integration: - ✅ Consolidate vite/vitest/oxlint/oxfmt dependencies → vite-plus - ✅ Merge tool configurations into vite.config.ts -- ❌ Does NOT migrate ESLint → oxlint (if template uses ESLint, it stays) +- ✅ Migrate ESLint → oxlint (via `@oxlint/migrate`) when the template ships with ESLint flat config +- ✅ Migrate Prettier → oxfmt when the template ships with Prettier - ❌ Does NOT create vite-task.json (optional, separate feature) - ❌ Does NOT change TypeScript config (remains as generated) @@ -143,8 +148,10 @@ Monorepo integration: │ ┌──────▼─────────────┐ │ Auto-Migrate │ - │ vite-tools │ - │ → vite-plus │ + │ • vite-tools │ + │ → vite-plus │ + │ • ESLint → oxlint │ + │ • Prettier → oxfmt │ └──────┬─────────────┘ │ ┌──────▼─────────────┐ @@ -159,11 +166,11 @@ Monorepo integration: After any template runs, Vite+ adds monorepo-specific features: -### 1. Auto-Migration to vite-plus Unified Toolchain (for ALL templates) +### 1. Auto-Migration to vite-plus Unified Toolchain + oxlint/oxfmt (for ALL templates) -**After any template runs** (bingo or universal), Vite+ automatically detects standalone vite-related tools and offers to consolidate them into the unified vite-plus dependency. +**After any template runs** (bingo or universal), Vite+ automatically detects standalone vite-related tools _and_ ESLint/Prettier, and migrates them to the unified Vite+ toolchain (vite-plus + oxlint + oxfmt). -**Purpose**: Simplify dependency management by consolidating vite, vitest, oxlint, and oxfmt into a single vite-plus package. +**Purpose**: Land the scaffolded project on the same toolchain `vp migrate` produces — so the user doesn't have to run `vp migrate` as a second step. ```bash $ vp create create-vite --template react-ts @@ -201,12 +208,20 @@ Scaffolding project in ./packages/my-app... │ ✓ Merged vitest.config.ts → vite.config.ts │ ✓ Removed vitest.config.ts │ +# Then Vite+ migrates ESLint → oxlint (template ships with eslint.config.js) +# No prompt — Vite+ is opinionated about oxlint, so migration runs automatically. +◇ Migrating ESLint → Oxlint... +│ ✓ Generated .oxlintrc.json from eslint.config.js +│ ✓ Rewrote `eslint-disable` comments to `oxlint-disable` +│ ✓ Removed eslint.config.js and eslint devDependency +│ ✓ Rewrote `"lint": "eslint ."` → `"lint": "vp lint"` +│ └ Migration completed! ``` **Scope of Auto-Migration**: -This is a **dependency consolidation** feature, not a tool replacement feature. +Combines **dependency consolidation** with **lint/format tool migration** — the same work `vp migrate` does on existing projects, applied automatically after scaffolding. ✅ **What it does**: @@ -215,20 +230,23 @@ This is a **dependency consolidation** feature, not a tool replacement feature. - Merge .oxlintrc → vite.config.ts (oxlint section) - Merge .oxfmtrc → vite.config.ts (oxfmt section) - Remove redundant standalone config files +- Migrate ESLint configs + dependency + scripts → oxlint (delegates to `@oxlint/migrate`) +- Migrate Prettier configs + dependency + scripts → oxfmt +- Rewrite lint-staged entries to `vp lint` / `vp fmt` ❌ **What it does NOT do**: -- Does NOT migrate ESLint → oxlint (different tools, not consolidation) -- Does NOT migrate Prettier → oxfmt (different tools, not consolidation) - Does NOT create vite-task.json (separate feature, not required) - Does NOT change TypeScript configuration (remains as generated) - Does NOT modify build tools (webpack/rollup → vite) +- Does NOT migrate legacy ESLint (`.eslintrc.*`) — prints a warning asking the user to upgrade to ESLint v9 flat config first, same as `vp migrate` **Why this design**: -- Templates that use vite/vitest/oxlint/oxfmt can be simplified to use vite-plus -- Templates that use other tools (ESLint, Prettier, Jest) remain unchanged -- Users keep their chosen tools, just with optimized vite-related dependencies +- Vite+ is opinionated about linting and formatting: oxlint + oxfmt are the default toolchain. A freshly scaffolded project should already be on that toolchain — making the user run `vp migrate` as a second step defeats the point. +- ESLint/Prettier migration runs **without a confirmation prompt** inside `vp create`, even in interactive mode. This differs from `vp migrate` (which prompts because the user has an existing project with their own preferences) — for a brand-new app the choice is already made by scaffolding onto Vite+. +- Reusing the `vp migrate` helpers keeps the spec and implementation in one place and guarantees parity with the migration command. +- Templates that use unrelated tools (Jest, webpack, rollup) stay untouched. **Migration Engine powered by [ast-grep](https://ast-grep.github.io/)**: @@ -987,6 +1005,12 @@ Vite+ acts as an intelligent wrapper that: ├─ Merge .oxfmtrc → vite.config.ts └─ Remove standalone config files + AUTO-MIGRATE LINT/FORMAT TOOLS (shared with vp migrate): + ├─ Detect ESLint flat config + dependency + ├─ Migrate to oxlint via @oxlint/migrate (configs, scripts, lint-staged) + ├─ Detect Prettier config + dependency + └─ Migrate to oxfmt (configs, scripts, lint-staged) + MONOREPO INTEGRATION: ├─ Prompt user to select workspace dependencies ├─ Update package.json with workspace:* dependencies @@ -2097,6 +2121,9 @@ A successful implementation should: 13. ✅ Provide clear before/after explanations 14. ✅ Be safe and reversible 15. ⏳ Merge configurations (vitest.config.ts, .oxlintrc, .oxfmtrc → vite.config.ts) - Future enhancement with ast-grep +16. ✅ Migrate ESLint configs / dependency / scripts to oxlint via `@oxlint/migrate` (shares helpers with `vp migrate`) +17. ✅ Migrate Prettier configs / dependency / scripts to oxfmt +18. ✅ Warn on legacy `.eslintrc.*` and skip migration (asks the user to upgrade to ESLint v9 flat config first) ### Monorepo Integration @@ -2161,6 +2188,7 @@ A successful implementation should: - Shares the same migration engine and rules - `vp create` runs migrations after template generation - `vp migrate` runs migrations on existing projects + - ESLint → oxlint and Prettier → oxfmt migration helpers live in `packages/cli/src/migration/` and are invoked by both commands, so a freshly scaffolded project and an upgraded existing project end up in the same state ## References From 9c7695e06023cddd7eedb16eafb8d8a20e5f9dae Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 21 Apr 2026 23:01:52 +0800 Subject: [PATCH 02/11] test(cli/create): add snap test verifying ESLint auto-migration The existing `new-create-vite` snap test runs with VP_SKIP_INSTALL=1 (set globally by the snap-test tool), so the new install-gated migration path in vp create is a no-op there. Add a dedicated snap test that unsets VP_SKIP_INSTALL, runs `vp create --template react-ts` end-to-end, and asserts the post-migration state: - .oxlintrc.json generated - eslint.config.js deleted - package.json lint script rewritten to `vp lint .` - eslint devDependency removed --- .../snap.txt | 10 ++++++++++ .../steps.json | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt create mode 100644 packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json diff --git a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt new file mode 100644 index 0000000000..fbc78e8383 --- /dev/null +++ b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt @@ -0,0 +1,10 @@ +> vp create vite:application --no-interactive --directory my-react-ts -- --template react-ts # create vite app with react-ts template, should auto-migrate ESLint -> Oxlint +> test -f my-react-ts/.oxlintrc.json && echo '.oxlintrc.json exists' # migration generated oxlint config +.oxlintrc.json exists + +> test ! -f my-react-ts/eslint.config.js && echo 'eslint.config.js removed' # eslint config deleted +eslint.config.js removed + +> node -e "const p=require('./my-react-ts/package.json');console.log('lint:', p.scripts && p.scripts.lint);console.log('eslint dep:', !!(p.devDependencies && p.devDependencies.eslint));" # scripts rewritten, eslint dep removed +lint: vp lint . +eslint dep: false diff --git a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json new file mode 100644 index 0000000000..2d6f0c7cd1 --- /dev/null +++ b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json @@ -0,0 +1,16 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VP_SKIP_INSTALL": "", + "CI": "" + }, + "commands": [ + { + "command": "vp create vite:application --no-interactive --directory my-react-ts -- --template react-ts # create vite app with react-ts template, should auto-migrate ESLint -> Oxlint", + "ignoreOutput": true + }, + "test -f my-react-ts/.oxlintrc.json && echo '.oxlintrc.json exists' # migration generated oxlint config", + "test ! -f my-react-ts/eslint.config.js && echo 'eslint.config.js removed' # eslint config deleted", + "node -e \"const p=require('./my-react-ts/package.json');console.log('lint:', p.scripts && p.scripts.lint);console.log('eslint dep:', !!(p.devDependencies && p.devDependencies.eslint));\" # scripts rewritten, eslint dep removed" + ] +} From 269ace9a2022eafe2dc091986ba68b3fc740f9cd Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 21 Apr 2026 23:13:13 +0800 Subject: [PATCH 03/11] feat(cli/create): run ESLint/Prettier migration before the vite-plus rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback: the RFC and implementation were misordered — the "merge .oxlintrc/.oxfmtrc → vite.config.ts" step was described before the ESLint/Prettier migration that generates those configs, so in practice `.oxlintrc.json` ended up as a loose file instead of being merged, unlike `vp migrate`. Reorder the create pipeline to match `vp migrate`: scaffold → install #1 (template deps incl. ESLint plugins, so @oxlint/migrate can resolve imports from eslint.config.js) → migrate ESLint → oxlint / Prettier → oxfmt (generates .oxlintrc.json, .oxfmtrc.json) → rewrite (merges .oxlintrc/.oxfmtrc into vite.config.ts, swaps vite/vitest → vite-plus) → install #2 (fetches vite-plus, prunes eslint/prettier deps) → fmt End state now matches `vp migrate`: `.oxlintrc.json` is merged into `vite.config.ts` as a `lint: { ... }` section, eslint/prettier deps are gone, and `vite-plus` is installed. Update the new-create-vite-migrates-eslint-prettier snap test to assert the merged end state (`.oxlintrc.json` absent, `vite.config.ts` has a `lint:` section, `vite-plus` devDep present). Update the RFC flow diagrams and bullet lists to reflect the migrate-before-merge ordering. --- .../snap.txt | 14 ++-- .../steps.json | 7 +- packages/cli/src/create/bin.ts | 70 ++++++++++++------- rfcs/code-generator.md | 22 +++--- 4 files changed, 70 insertions(+), 43 deletions(-) diff --git a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt index fbc78e8383..c3d42477ff 100644 --- a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt +++ b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt @@ -1,10 +1,14 @@ -> vp create vite:application --no-interactive --directory my-react-ts -- --template react-ts # create vite app with react-ts template, should auto-migrate ESLint -> Oxlint -> test -f my-react-ts/.oxlintrc.json && echo '.oxlintrc.json exists' # migration generated oxlint config -.oxlintrc.json exists - +> vp create vite:application --no-interactive --directory my-react-ts -- --template react-ts # create vite app with react-ts template, should auto-migrate ESLint -> Oxlint and merge lint config into vite.config.ts > test ! -f my-react-ts/eslint.config.js && echo 'eslint.config.js removed' # eslint config deleted eslint.config.js removed -> node -e "const p=require('./my-react-ts/package.json');console.log('lint:', p.scripts && p.scripts.lint);console.log('eslint dep:', !!(p.devDependencies && p.devDependencies.eslint));" # scripts rewritten, eslint dep removed +> test ! -f my-react-ts/.oxlintrc.json && echo '.oxlintrc.json merged into vite.config.ts' # migration output merged by rewrite step (matches vp migrate) +.oxlintrc.json merged into vite.config.ts + +> grep -q 'lint:' my-react-ts/vite.config.ts && echo 'vite.config.ts has lint section' # oxlint config merged into vite.config.ts +vite.config.ts has lint section + +> node -e "const p=require('./my-react-ts/package.json');console.log('lint:', p.scripts && p.scripts.lint);console.log('eslint dep:', !!(p.devDependencies && p.devDependencies.eslint));console.log('vite-plus dep:', !!(p.devDependencies && p.devDependencies['vite-plus']));" # scripts rewritten, eslint dep removed, vite-plus added lint: vp lint . eslint dep: false +vite-plus dep: true diff --git a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json index 2d6f0c7cd1..fd2a134248 100644 --- a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json +++ b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json @@ -6,11 +6,12 @@ }, "commands": [ { - "command": "vp create vite:application --no-interactive --directory my-react-ts -- --template react-ts # create vite app with react-ts template, should auto-migrate ESLint -> Oxlint", + "command": "vp create vite:application --no-interactive --directory my-react-ts -- --template react-ts # create vite app with react-ts template, should auto-migrate ESLint -> Oxlint and merge lint config into vite.config.ts", "ignoreOutput": true }, - "test -f my-react-ts/.oxlintrc.json && echo '.oxlintrc.json exists' # migration generated oxlint config", "test ! -f my-react-ts/eslint.config.js && echo 'eslint.config.js removed' # eslint config deleted", - "node -e \"const p=require('./my-react-ts/package.json');console.log('lint:', p.scripts && p.scripts.lint);console.log('eslint dep:', !!(p.devDependencies && p.devDependencies.eslint));\" # scripts rewritten, eslint dep removed" + "test ! -f my-react-ts/.oxlintrc.json && echo '.oxlintrc.json merged into vite.config.ts' # migration output merged by rewrite step (matches vp migrate)", + "grep -q 'lint:' my-react-ts/vite.config.ts && echo 'vite.config.ts has lint section' # oxlint config merged into vite.config.ts", + "node -e \"const p=require('./my-react-ts/package.json');console.log('lint:', p.scripts && p.scripts.lint);console.log('eslint dep:', !!(p.devDependencies && p.devDependencies.eslint));console.log('vite-plus dep:', !!(p.devDependencies && p.devDependencies['vite-plus']));\" # scripts rewritten, eslint dep removed, vite-plus added" ] } diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index 644239d4a2..315d442558 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -900,13 +900,6 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h if (!compactOutput) { prompts.log.step('Monorepo integration...'); } - updateCreateProgress('Integrating into monorepo'); - rewriteMonorepoProject(fullPath, workspaceInfo.packageManager, undefined, compactOutput); - for (const framework of detectFramework(fullPath)) { - if (!hasFrameworkShim(fullPath, framework)) { - addFrameworkShim(fullPath, framework); - } - } if (workspaceInfo.packages.length > 0) { if (options.interactive) { @@ -967,16 +960,19 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h } updateWorkspaceConfig(projectDir, workspaceInfo); + // First install: template deps (incl. ESLint plugins) so `@oxlint/migrate` + // can resolve imports from eslint.config.js during migration below. updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(workspaceInfo.rootDir, options.interactive, installArgs, { silent: compactOutput, }); - // Auto-migrate ESLint → Oxlint and Prettier → Oxfmt after install so - // `@oxlint/migrate` can resolve the template's ESLint plugin packages. - // Runs unconditionally (Vite+ is opinionated about oxlint/oxfmt — - // freshly scaffolded projects should already be on the unified toolchain). - // Gated on install success — skipping when install was bypassed (e.g. - // VP_SKIP_INSTALL in snap tests) avoids failing module resolution. + // Auto-migrate ESLint → Oxlint and Prettier → Oxfmt BEFORE the rewrite + // step so the generated .oxlintrc.json / .oxfmtrc.json get merged into + // vite.config.ts by `rewriteMonorepoProject` below — matching `vp migrate`. + // Always non-interactive: Vite+ is opinionated about oxlint/oxfmt, so + // freshly scaffolded projects land on the unified toolchain by default. + // Gated on install success so VP_SKIP_INSTALL snap tests don't hit + // ERR_MODULE_NOT_FOUND when the plugins aren't on disk. if (installSummary.status === 'installed') { updateCreateProgress('Migrating lint and format tools'); pauseCreateProgress(); @@ -984,11 +980,44 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h await promptPrettierMigration(fullPath, /* interactive */ false); resumeCreateProgress(); } + updateCreateProgress('Integrating into monorepo'); + rewriteMonorepoProject(fullPath, workspaceInfo.packageManager, undefined, compactOutput); + for (const framework of detectFramework(fullPath)) { + if (!hasFrameworkShim(fullPath, framework)) { + addFrameworkShim(fullPath, framework); + } + } + // Second install: fetch vite-plus (added by rewrite) and prune the + // eslint/prettier deps that migration removed from package.json. + updateCreateProgress('Installing dependencies'); + installSummary = await runViteInstall(workspaceInfo.rootDir, options.interactive, installArgs, { + silent: compactOutput, + }); updateCreateProgress('Formatting code'); await runViteFmt(workspaceInfo.rootDir, options.interactive, [projectDir], { silent: compactOutput, }); } else { + // First install: template deps (incl. ESLint plugins) so `@oxlint/migrate` + // can resolve imports from eslint.config.js during migration below. + updateCreateProgress('Installing dependencies'); + installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { + silent: compactOutput, + }); + // Auto-migrate ESLint → Oxlint and Prettier → Oxfmt BEFORE the rewrite + // step so the generated .oxlintrc.json / .oxfmtrc.json get merged into + // vite.config.ts by `rewriteStandaloneProject` below — matching `vp migrate`. + // Always non-interactive: Vite+ is opinionated about oxlint/oxfmt, so + // freshly scaffolded projects land on the unified toolchain by default. + // Gated on install success so VP_SKIP_INSTALL snap tests don't hit + // ERR_MODULE_NOT_FOUND when the plugins aren't on disk. + if (installSummary.status === 'installed') { + updateCreateProgress('Migrating lint and format tools'); + pauseCreateProgress(); + await promptEslintMigration(fullPath, /* interactive */ false); + await promptPrettierMigration(fullPath, /* interactive */ false); + resumeCreateProgress(); + } updateCreateProgress('Applying Vite+ project setup'); rewriteStandaloneProject(fullPath, workspaceInfo, undefined, compactOutput); for (const framework of detectFramework(fullPath)) { @@ -999,23 +1028,12 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h if (shouldSetupHooks) { installGitHooks(fullPath, compactOutput); } + // Second install: fetch vite-plus (added by rewrite) and prune the + // eslint/prettier deps that migration removed from package.json. updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { silent: compactOutput, }); - // Auto-migrate ESLint → Oxlint and Prettier → Oxfmt after install so - // `@oxlint/migrate` can resolve the template's ESLint plugin packages. - // Runs unconditionally (Vite+ is opinionated about oxlint/oxfmt — - // freshly scaffolded projects should already be on the unified toolchain). - // Gated on install success — skipping when install was bypassed (e.g. - // VP_SKIP_INSTALL in snap tests) avoids failing module resolution. - if (installSummary.status === 'installed') { - updateCreateProgress('Migrating lint and format tools'); - pauseCreateProgress(); - await promptEslintMigration(fullPath, /* interactive */ false); - await promptPrettierMigration(fullPath, /* interactive */ false); - resumeCreateProgress(); - } updateCreateProgress('Formatting code'); await runViteFmt(fullPath, options.interactive, undefined, { silent: compactOutput }); } diff --git a/rfcs/code-generator.md b/rfcs/code-generator.md index 69c1314eb2..0b7d4a2c66 100644 --- a/rfcs/code-generator.md +++ b/rfcs/code-generator.md @@ -92,10 +92,10 @@ Vite+ auto-detects vite-related tools + lint/format tools: • ESLint (flat config) and Prettier ↓ Auto-migrate to unified vite-plus: + • ESLint → oxlint (via @oxlint/migrate) — generates .oxlintrc.json + • Prettier → oxfmt — generates .oxfmtrc.json • Dependencies: vite + vitest + oxlint + oxfmt → vite-plus • Configs: Merge vitest.config.ts, .oxlintrc, .oxfmtrc → vite.config.ts - • ESLint → oxlint (via @oxlint/migrate) - • Prettier → oxfmt • Rewrite lint-staged entries to vp lint / vp fmt ↓ Monorepo integration: @@ -995,22 +995,26 @@ Vite+ acts as an intelligent wrapper that: ↓ 14. Vite+ post-processes in detected project directory (same for ALL templates): + AUTO-MIGRATE LINT/FORMAT TOOLS (shared with vp migrate, runs first + so .oxlintrc.json / .oxfmtrc.json exist before the merge step below): + ├─ Detect ESLint flat config + dependency + ├─ Migrate to oxlint via @oxlint/migrate (generates .oxlintrc.json, + │ rewrites scripts, rewrites lint-staged) + ├─ Detect Prettier config + dependency + └─ Migrate to oxfmt (generates .oxfmtrc.json, rewrites scripts, + rewrites lint-staged) + AUTO-MIGRATE TO VITE-PLUS: ├─ Detect standalone vite/vitest/oxlint/oxfmt ├─ Prompt to upgrade to vite-plus unified toolchain └─ If yes, apply migration with ast-grep: ├─ Dependencies: vite + vitest + oxlint + oxfmt → vite-plus ├─ Merge vitest.config.ts → vite.config.ts - ├─ Merge .oxlintrc → vite.config.ts + ├─ Merge .oxlintrc → vite.config.ts (picks up the file + │ generated by the lint migration above) ├─ Merge .oxfmtrc → vite.config.ts └─ Remove standalone config files - AUTO-MIGRATE LINT/FORMAT TOOLS (shared with vp migrate): - ├─ Detect ESLint flat config + dependency - ├─ Migrate to oxlint via @oxlint/migrate (configs, scripts, lint-staged) - ├─ Detect Prettier config + dependency - └─ Migrate to oxfmt (configs, scripts, lint-staged) - MONOREPO INTEGRATION: ├─ Prompt user to select workspace dependencies ├─ Update package.json with workspace:* dependencies From 6854fd112f80a994061a9fde1893527b3111cd31 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 21 Apr 2026 23:18:07 +0800 Subject: [PATCH 04/11] test(cli/create): pin create-vite version, cat vite.config.ts, run on windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on the new-create-vite-migrates-eslint-prettier snap test: - Switch from `vp create vite:application --directory my-react-ts` to `vp create vite@9.0.5 -- my-react-ts --template react-ts`. Pinning create-vite to 9.0.5 makes the snap stable against upstream template changes, and the project name is passed as a positional (which is what create-vite expects; `--directory` is rejected for remote templates). - Add `cat my-react-ts/vite.config.ts` so the snap captures the full merged config (oxlint `lint: { ... }` block + `fmt: {}` section), making the parity with `vp migrate` directly visible in diffs. - Drop `ignoredPlatforms: ["win32"]` — nothing in this flow is Windows-specific. --- .../snap.txt | 127 +++++++++++++++++- .../steps.json | 5 +- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt index c3d42477ff..dd2e928386 100644 --- a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt +++ b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt @@ -1,12 +1,133 @@ -> vp create vite:application --no-interactive --directory my-react-ts -- --template react-ts # create vite app with react-ts template, should auto-migrate ESLint -> Oxlint and merge lint config into vite.config.ts +> vp create vite@9.0.5 --no-interactive -- my-react-ts --template react-ts # create vite app with pinned version + react-ts template, should auto-migrate ESLint -> Oxlint and merge lint config into vite.config.ts > test ! -f my-react-ts/eslint.config.js && echo 'eslint.config.js removed' # eslint config deleted eslint.config.js removed > test ! -f my-react-ts/.oxlintrc.json && echo '.oxlintrc.json merged into vite.config.ts' # migration output merged by rewrite step (matches vp migrate) .oxlintrc.json merged into vite.config.ts -> grep -q 'lint:' my-react-ts/vite.config.ts && echo 'vite.config.ts has lint section' # oxlint config merged into vite.config.ts -vite.config.ts has lint section +> cat my-react-ts/vite.config.ts # merged vite config should contain lint and fmt sections +import { defineConfig } from "vite-plus"; +import react from "@vitejs/plugin-react"; + +// https://vite.dev/config/ +export default defineConfig({ + staged: { + "*": "vp check --fix", + }, + fmt: {}, + lint: { + plugins: ["oxc", "typescript", "unicorn", "react"], + categories: { + correctness: "warn", + }, + env: { + builtin: true, + }, + ignorePatterns: ["dist"], + overrides: [ + { + files: ["**/*.{ts,tsx}"], + rules: { + "constructor-super": "error", + "for-direction": "error", + "getter-return": "error", + "no-async-promise-executor": "error", + "no-case-declarations": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-const-assign": "error", + "no-constant-binary-expression": "error", + "no-constant-condition": "error", + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-class-members": "error", + "no-dupe-else-if": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": "error", + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-empty-static-block": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-fallthrough": "error", + "no-func-assign": "error", + "no-global-assign": "error", + "no-import-assign": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-loss-of-precision": "error", + "no-misleading-character-class": "error", + "no-new-native-nonconstructor": "error", + "no-nonoctal-decimal-escape": "error", + "no-obj-calls": "error", + "no-prototype-builtins": "error", + "no-redeclare": "error", + "no-regex-spaces": "error", + "no-self-assign": "error", + "no-setter-return": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-this-before-super": "error", + "no-undef": "error", + "no-unexpected-multiline": "error", + "no-unreachable": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-unsafe-optional-chaining": "error", + "no-unused-labels": "error", + "no-unused-private-class-members": "error", + "no-unused-vars": "error", + "no-useless-backreference": "error", + "no-useless-catch": "error", + "no-useless-escape": "error", + "no-with": "error", + "require-yield": "error", + "use-isnan": "error", + "valid-typeof": "error", + "no-array-constructor": "error", + "no-unused-expressions": "error", + "typescript/ban-ts-comment": "error", + "typescript/no-duplicate-enum-values": "error", + "typescript/no-empty-object-type": "error", + "typescript/no-explicit-any": "error", + "typescript/no-extra-non-null-assertion": "error", + "typescript/no-misused-new": "error", + "typescript/no-namespace": "error", + "typescript/no-non-null-asserted-optional-chain": "error", + "typescript/no-require-imports": "error", + "typescript/no-this-alias": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unsafe-declaration-merging": "error", + "typescript/no-unsafe-function-type": "error", + "typescript/no-wrapper-object-types": "error", + "typescript/prefer-as-const": "error", + "typescript/prefer-namespace-keyword": "error", + "typescript/triple-slash-reference": "error", + "react/rules-of-hooks": "error", + "react/exhaustive-deps": "warn", + "react/only-export-components": [ + "error", + { + allowConstantExport: true, + }, + ], + }, + env: { + es2020: true, + browser: true, + }, + }, + ], + options: { + typeAware: true, + typeCheck: true, + }, + }, + plugins: [react()], +}); > node -e "const p=require('./my-react-ts/package.json');console.log('lint:', p.scripts && p.scripts.lint);console.log('eslint dep:', !!(p.devDependencies && p.devDependencies.eslint));console.log('vite-plus dep:', !!(p.devDependencies && p.devDependencies['vite-plus']));" # scripts rewritten, eslint dep removed, vite-plus added lint: vp lint . diff --git a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json index fd2a134248..e1fde38fa5 100644 --- a/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json +++ b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json @@ -1,17 +1,16 @@ { - "ignoredPlatforms": ["win32"], "env": { "VP_SKIP_INSTALL": "", "CI": "" }, "commands": [ { - "command": "vp create vite:application --no-interactive --directory my-react-ts -- --template react-ts # create vite app with react-ts template, should auto-migrate ESLint -> Oxlint and merge lint config into vite.config.ts", + "command": "vp create vite@9.0.5 --no-interactive -- my-react-ts --template react-ts # create vite app with pinned version + react-ts template, should auto-migrate ESLint -> Oxlint and merge lint config into vite.config.ts", "ignoreOutput": true }, "test ! -f my-react-ts/eslint.config.js && echo 'eslint.config.js removed' # eslint config deleted", "test ! -f my-react-ts/.oxlintrc.json && echo '.oxlintrc.json merged into vite.config.ts' # migration output merged by rewrite step (matches vp migrate)", - "grep -q 'lint:' my-react-ts/vite.config.ts && echo 'vite.config.ts has lint section' # oxlint config merged into vite.config.ts", + "cat my-react-ts/vite.config.ts # merged vite config should contain lint and fmt sections", "node -e \"const p=require('./my-react-ts/package.json');console.log('lint:', p.scripts && p.scripts.lint);console.log('eslint dep:', !!(p.devDependencies && p.devDependencies.eslint));console.log('vite-plus dep:', !!(p.devDependencies && p.devDependencies['vite-plus']));\" # scripts rewritten, eslint dep removed, vite-plus added" ] } From 23ae4c590203163a3b6879a458ff5495c7846c90 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 21 Apr 2026 23:24:26 +0800 Subject: [PATCH 05/11] refactor(cli/create): dedupe migrate block; drop misleading recovery hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify review follow-ups: - Extract the install-gated migrate block into a local `migrateLintFmtTools` arrow so the standalone and monorepo branches stop duplicating the same 7-line sequence (install status check → progress update → pause/resume → two awaits). The WHY comment now lives once above the helper instead of being copy-pasted above each call site. - Trim the redundant second-install WHAT comment to a single-line WHY (fetch vite-plus added by rewrite, prune removed eslint/prettier). - Drop "Fix the issue and re-run `vp migrate`" from `promptEslintMigration` / `promptPrettierMigration` failure messages. These helpers are now invoked by both `vp create` and `vp migrate`, so the recovery hint was misleading for create-context callers. --- packages/cli/src/create/bin.ts | 59 ++++++++++---------------- packages/cli/src/migration/migrator.ts | 4 +- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index 315d442558..03d503d2b8 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -895,6 +895,23 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h }); resumeCreateProgress(); + // Auto-migrate ESLint → Oxlint and Prettier → Oxfmt after install #1 so + // `@oxlint/migrate` can resolve imports from eslint.config.js, but before + // the vite-plus rewrite so the generated .oxlintrc/.oxfmtrc get merged + // into vite.config.ts — matching `vp migrate`. Always non-interactive; + // Vite+ is opinionated about oxlint/oxfmt. Gated on install success so + // VP_SKIP_INSTALL snap tests don't hit ERR_MODULE_NOT_FOUND. + const migrateLintFmtTools = async (installed: boolean) => { + if (!installed) { + return; + } + updateCreateProgress('Migrating lint and format tools'); + pauseCreateProgress(); + await promptEslintMigration(fullPath, /* interactive */ false); + await promptPrettierMigration(fullPath, /* interactive */ false); + resumeCreateProgress(); + }; + let installSummary: CommandRunSummary | undefined; if (isMonorepo) { if (!compactOutput) { @@ -960,26 +977,11 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h } updateWorkspaceConfig(projectDir, workspaceInfo); - // First install: template deps (incl. ESLint plugins) so `@oxlint/migrate` - // can resolve imports from eslint.config.js during migration below. updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(workspaceInfo.rootDir, options.interactive, installArgs, { silent: compactOutput, }); - // Auto-migrate ESLint → Oxlint and Prettier → Oxfmt BEFORE the rewrite - // step so the generated .oxlintrc.json / .oxfmtrc.json get merged into - // vite.config.ts by `rewriteMonorepoProject` below — matching `vp migrate`. - // Always non-interactive: Vite+ is opinionated about oxlint/oxfmt, so - // freshly scaffolded projects land on the unified toolchain by default. - // Gated on install success so VP_SKIP_INSTALL snap tests don't hit - // ERR_MODULE_NOT_FOUND when the plugins aren't on disk. - if (installSummary.status === 'installed') { - updateCreateProgress('Migrating lint and format tools'); - pauseCreateProgress(); - await promptEslintMigration(fullPath, /* interactive */ false); - await promptPrettierMigration(fullPath, /* interactive */ false); - resumeCreateProgress(); - } + await migrateLintFmtTools(installSummary.status === 'installed'); updateCreateProgress('Integrating into monorepo'); rewriteMonorepoProject(fullPath, workspaceInfo.packageManager, undefined, compactOutput); for (const framework of detectFramework(fullPath)) { @@ -987,8 +989,8 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h addFrameworkShim(fullPath, framework); } } - // Second install: fetch vite-plus (added by rewrite) and prune the - // eslint/prettier deps that migration removed from package.json. + // Re-install so the package manager fetches vite-plus (added by the + // rewrite) and prunes the eslint/prettier deps that migration removed. updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(workspaceInfo.rootDir, options.interactive, installArgs, { silent: compactOutput, @@ -998,26 +1000,11 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h silent: compactOutput, }); } else { - // First install: template deps (incl. ESLint plugins) so `@oxlint/migrate` - // can resolve imports from eslint.config.js during migration below. updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { silent: compactOutput, }); - // Auto-migrate ESLint → Oxlint and Prettier → Oxfmt BEFORE the rewrite - // step so the generated .oxlintrc.json / .oxfmtrc.json get merged into - // vite.config.ts by `rewriteStandaloneProject` below — matching `vp migrate`. - // Always non-interactive: Vite+ is opinionated about oxlint/oxfmt, so - // freshly scaffolded projects land on the unified toolchain by default. - // Gated on install success so VP_SKIP_INSTALL snap tests don't hit - // ERR_MODULE_NOT_FOUND when the plugins aren't on disk. - if (installSummary.status === 'installed') { - updateCreateProgress('Migrating lint and format tools'); - pauseCreateProgress(); - await promptEslintMigration(fullPath, /* interactive */ false); - await promptPrettierMigration(fullPath, /* interactive */ false); - resumeCreateProgress(); - } + await migrateLintFmtTools(installSummary.status === 'installed'); updateCreateProgress('Applying Vite+ project setup'); rewriteStandaloneProject(fullPath, workspaceInfo, undefined, compactOutput); for (const framework of detectFramework(fullPath)) { @@ -1028,8 +1015,8 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h if (shouldSetupHooks) { installGitHooks(fullPath, compactOutput); } - // Second install: fetch vite-plus (added by rewrite) and prune the - // eslint/prettier deps that migration removed from package.json. + // Re-install so the package manager fetches vite-plus (added by the + // rewrite) and prunes the eslint/prettier deps that migration removed. updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { silent: compactOutput, diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts index 6afc67aa13..510f40bde2 100644 --- a/packages/cli/src/migration/migrator.ts +++ b/packages/cli/src/migration/migrator.ts @@ -2588,7 +2588,7 @@ export async function promptEslintMigration( packages, ); if (!ok) { - cancelAndExit('ESLint migration failed. Fix the issue and re-run `vp migrate`.', 1); + cancelAndExit('ESLint migration failed.', 1); } return true; } @@ -2644,7 +2644,7 @@ export async function promptPrettierMigration( packages, ); if (!ok) { - cancelAndExit('Prettier migration failed. Fix the issue and re-run `vp migrate`.', 1); + cancelAndExit('Prettier migration failed.', 1); } return true; } From d3f32c54a20931adb361bf5a5f00e961685342db Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 21 Apr 2026 23:39:02 +0800 Subject: [PATCH 06/11] fix(cli/create): only reorder install when template ships ESLint/Prettier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI failure: vp create library (npm/yarn/bun) was failing with ERR_PNPM_NO_MATCHING_VERSION for @voidzero-dev/vite-plus-core@0.0.0. The previous commit added an unconditional pre-rewrite install so that `@oxlint/migrate` could resolve the template's ESLint plugin imports. That broke builtin templates (vite:library, vite:application, vite:monorepo) — their scaffold already references vite-plus and relies on `rewrite*Project` to add the tarball overrides BEFORE the first install. Running install first made pnpm fall back to the npm registry for vite-plus-core@0.0.0, which CI publishes only as a local tarball. Gate the migrate-before-rewrite reorder on `detectEslintProject` / `detectPrettierProject`. Builtin templates have neither, so they keep the original rewrite → install → fmt flow (unchanged from main). Remote templates like `create-vite --template react-ts` do ship ESLint/Prettier, so they still get the reorder that merges .oxlintrc /.oxfmtrc into vite.config.ts. --- packages/cli/src/create/bin.ts | 42 ++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index 03d503d2b8..e8b2355d89 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -7,7 +7,9 @@ import mri from 'mri'; import { vitePlusHeader } from '../../binding/index.js'; import { addFrameworkShim, + detectEslintProject, detectFramework, + detectPrettierProject, hasFrameworkShim, installGitHooks, promptEslintMigration, @@ -912,6 +914,15 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h resumeCreateProgress(); }; + // The migrate-before-rewrite reorder is only needed when the template + // actually ships ESLint or Prettier (e.g. `create-vite --template + // react-ts`). Builtin templates (vite:library, vite:application, + // vite:monorepo) don't — their package.json already references vite-plus + // and relies on `rewrite*Project` to add tarball overrides BEFORE the + // first install, so install-first would break CI's local-tarball resolve. + const shouldMigrateLintFmtTools = + detectEslintProject(fullPath).hasDependency || detectPrettierProject(fullPath).hasDependency; + let installSummary: CommandRunSummary | undefined; if (isMonorepo) { if (!compactOutput) { @@ -977,11 +988,16 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h } updateWorkspaceConfig(projectDir, workspaceInfo); - updateCreateProgress('Installing dependencies'); - installSummary = await runViteInstall(workspaceInfo.rootDir, options.interactive, installArgs, { - silent: compactOutput, - }); - await migrateLintFmtTools(installSummary.status === 'installed'); + if (shouldMigrateLintFmtTools) { + updateCreateProgress('Installing dependencies'); + installSummary = await runViteInstall( + workspaceInfo.rootDir, + options.interactive, + installArgs, + { silent: compactOutput }, + ); + await migrateLintFmtTools(installSummary.status === 'installed'); + } updateCreateProgress('Integrating into monorepo'); rewriteMonorepoProject(fullPath, workspaceInfo.packageManager, undefined, compactOutput); for (const framework of detectFramework(fullPath)) { @@ -989,8 +1005,6 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h addFrameworkShim(fullPath, framework); } } - // Re-install so the package manager fetches vite-plus (added by the - // rewrite) and prunes the eslint/prettier deps that migration removed. updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(workspaceInfo.rootDir, options.interactive, installArgs, { silent: compactOutput, @@ -1000,11 +1014,13 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h silent: compactOutput, }); } else { - updateCreateProgress('Installing dependencies'); - installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { - silent: compactOutput, - }); - await migrateLintFmtTools(installSummary.status === 'installed'); + if (shouldMigrateLintFmtTools) { + updateCreateProgress('Installing dependencies'); + installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { + silent: compactOutput, + }); + await migrateLintFmtTools(installSummary.status === 'installed'); + } updateCreateProgress('Applying Vite+ project setup'); rewriteStandaloneProject(fullPath, workspaceInfo, undefined, compactOutput); for (const framework of detectFramework(fullPath)) { @@ -1015,8 +1031,6 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h if (shouldSetupHooks) { installGitHooks(fullPath, compactOutput); } - // Re-install so the package manager fetches vite-plus (added by the - // rewrite) and prunes the eslint/prettier deps that migration removed. updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { silent: compactOutput, From ac9b04a94b0dac74082b001abd9d801164c6b0e9 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 22 Apr 2026 09:36:45 +0800 Subject: [PATCH 07/11] ci(test-vp-create): add remote vite@9.0.5 + react-ts migration matrix entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing matrix only covers builtin templates (vite:monorepo, vite:application, vite:library) — none of which ship ESLint or Prettier, so the recent ESLint/Prettier auto-migration path in `vp create` goes untested by this workflow. Add `remote-vite-react-ts` that scaffolds with `vp create vite@9.0.5 -- test-project --template react-ts` (pinned so the snap is stable against upstream template changes) and asserts the post-migration end state: - eslint.config.js deleted - .oxlintrc.json merged into vite.config.ts (not left loose) - vite.config.ts contains a `lint:` section - package.json: eslint devDep removed, vite-plus devDep present, `lint` script rewritten to `vp lint …` The verification step is gated on a new matrix `verify-migration` field so builtin entries skip it. All four package managers (pnpm, npm, yarn, bun) exercise the path. --- .github/workflows/test-vp-create.yml | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/.github/workflows/test-vp-create.yml b/.github/workflows/test-vp-create.yml index 72e3644aa0..24c53cff81 100644 --- a/.github/workflows/test-vp-create.yml +++ b/.github/workflows/test-vp-create.yml @@ -123,16 +123,29 @@ jobs: create-args: vite:monorepo --directory test-project template-args: '' verify-command: vp run ready + verify-migration: 'false' - name: application create-args: vite:application --directory test-project template-args: '-- --template vanilla-ts' verify-command: vp run build + verify-migration: 'false' - name: library create-args: vite:library --directory test-project template-args: '' verify-command: | vp run build vp run test + verify-migration: 'false' + # Remote template that ships ESLint (+ an eslint.config.js importing + # @eslint/js etc.). Exercises the migrate-before-rewrite reorder in + # `vp create`: after scaffold, ESLint → oxlint and Prettier → oxfmt + # run before the vite-plus rewrite so `.oxlintrc` / `.oxfmtrc` get + # merged into vite.config.ts. + - name: remote-vite-react-ts + create-args: vite@9.0.5 + template-args: '-- test-project --template react-ts' + verify-command: vp run build + verify-migration: 'true' package-manager: - pnpm - npm @@ -253,6 +266,41 @@ jobs: console.log('✓ vite-plus@' + pkg.version + ' installed correctly'); " + - name: Verify ESLint/Prettier auto-migration + if: matrix.template.verify-migration == 'true' + working-directory: ${{ runner.temp }}/test-project + run: | + # eslint.config.js must be gone (migration deleted it) + test ! -f eslint.config.js + echo "✓ eslint.config.js removed" + + # .oxlintrc.json must NOT be loose on disk — it was merged into + # vite.config.ts by the rewrite step that runs after migration. + test ! -f .oxlintrc.json + echo "✓ .oxlintrc.json merged into vite.config.ts" + + # vite.config.ts must contain the merged oxlint config. + grep -q '^[[:space:]]*lint:' vite.config.ts + echo "✓ vite.config.ts has merged lint section" + + # package.json: eslint devDep removed, vite-plus present, lint script rewritten. + node -e " + const pkg = require('./package.json'); + if (pkg.devDependencies && pkg.devDependencies.eslint) { + console.error('✗ eslint devDependency should have been removed'); + process.exit(1); + } + if (!pkg.devDependencies || !pkg.devDependencies['vite-plus']) { + console.error('✗ vite-plus devDependency missing'); + process.exit(1); + } + if (!pkg.scripts || !pkg.scripts.lint || !pkg.scripts.lint.includes('vp lint')) { + console.error('✗ lint script should invoke vp lint, got: ' + (pkg.scripts && pkg.scripts.lint)); + process.exit(1); + } + console.log('✓ package.json migrated (eslint gone, vite-plus added, lint script rewritten)'); + " + - name: Run vp check working-directory: ${{ runner.temp }}/test-project run: vp check From fd0e695ab2ea6bf41ea159b9d1b5004b7072ea0a Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 22 Apr 2026 09:54:37 +0800 Subject: [PATCH 08/11] ci(test-vp-create): scope remote-vite-react-ts matrix to pnpm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The newly added `remote-vite-react-ts` matrix entry passes on pnpm but fails on npm/yarn/bun with ERR_PNPM_NO_MATCHING_VERSION for @voidzero-dev/vite-plus-core@0.0.0 — even in the (npm) job. Root cause: create-vite@9.0.5 writes `"packageManager": "pnpm@..."` into the scaffolded package.json, which `vp install` follows over the `--package-manager` CLI flag. Combined with the local-tarball overrides CI uses (which are written in a form pnpm-standalone won't consult on the second install), install #2 falls back to the npm registry for vite-plus-core@0.0.0 and fails. Wiring --package-manager through remote-template scaffolds (and updating the overrides injection to match) is separate infrastructure work. For now, exclude npm/yarn/bun combinations via a matrix `exclude` so the regression coverage we gain on pnpm is still in CI without red builds on the other three. --- .github/workflows/test-vp-create.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/test-vp-create.yml b/.github/workflows/test-vp-create.yml index 24c53cff81..81e48aee65 100644 --- a/.github/workflows/test-vp-create.yml +++ b/.github/workflows/test-vp-create.yml @@ -151,6 +151,23 @@ jobs: - npm - yarn - bun + # Remote create-vite scaffolds bake `"packageManager": "pnpm@..."` + # into the generated package.json, which `vp install` follows over + # the --package-manager CLI flag. Combined with local-tarball + # overrides in CI, that makes non-pnpm paths resolve + # @voidzero-dev/vite-plus-core@0.0.0 from the npm registry and + # fail. Scope this matrix entry to pnpm until the remote-template + # install-flag propagation is sorted out separately. + exclude: + - template: + name: remote-vite-react-ts + package-manager: npm + - template: + name: remote-vite-react-ts + package-manager: yarn + - template: + name: remote-vite-react-ts + package-manager: bun env: VP_OVERRIDE_PACKAGES: '{"vite":"file:${{ github.workspace }}/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz","vitest":"file:${{ github.workspace }}/tmp/tgz/voidzero-dev-vite-plus-test-0.0.0.tgz","@voidzero-dev/vite-plus-core":"file:${{ github.workspace }}/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz","@voidzero-dev/vite-plus-test":"file:${{ github.workspace }}/tmp/tgz/voidzero-dev-vite-plus-test-0.0.0.tgz"}' VP_VERSION: 'file:${{ github.workspace }}/tmp/tgz/vite-plus-0.0.0.tgz' From c6b1944baa07fbfe5c779e65245ad872b4ab20a6 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 22 Apr 2026 10:11:16 +0800 Subject: [PATCH 09/11] fix(cli/create): pin packageManager and yarn linker before migrate install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remote-template migrate flow was breaking on npm/yarn/bun because `vp install` couldn't honor `--package-manager`: - `vite_install`'s `prompt_package_manager_selection` hardcodes pnpm in CI / non-TTY environments when no `packageManager` field, no workspace file, and no lockfile are present. create-vite scaffolds without a `packageManager` field, so every non-pnpm path silently switched to pnpm at install #1 — leaving the wrong lockfile for install #2 and tripping ERR_PNPM_NO_MATCHING_VERSION on CI's local tarballs. - Yarn Berry's default Plug'n'Play stores deps inside .yarn/cache/*.zip. `@oxlint/migrate` calls fileURLToPath over zip entries and throws MODULE_NOT_FOUND on `@eslint/js`. Before install #1 in the migrate-gated branches (standalone + monorepo): - Call `setPackageManager(fullPath, downloadPackageManager)` so `vp install` picks up the `--package-manager` choice from the field. - For yarn, write a minimal `.yarnrc.yml` with `nodeLinker: node-modules` so the install produces a classic `node_modules/` that `@oxlint/migrate` can resolve. Export `setPackageManager` from migrator.ts for reuse from create/bin.ts. Verified end-to-end locally: all four package managers (pnpm / npm / yarn / bun) now migrate `vite@9.0.5 --template react-ts` cleanly — eslint.config.js removed, `.oxlintrc.json` merged into vite.config.ts, packageManager pinned to the chosen PM. Drop the CI matrix `exclude` that previously scoped this entry to pnpm. --- .github/workflows/test-vp-create.yml | 17 --------------- packages/cli/src/create/bin.ts | 30 ++++++++++++++++++++++++++ packages/cli/src/migration/migrator.ts | 2 +- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test-vp-create.yml b/.github/workflows/test-vp-create.yml index 81e48aee65..24c53cff81 100644 --- a/.github/workflows/test-vp-create.yml +++ b/.github/workflows/test-vp-create.yml @@ -151,23 +151,6 @@ jobs: - npm - yarn - bun - # Remote create-vite scaffolds bake `"packageManager": "pnpm@..."` - # into the generated package.json, which `vp install` follows over - # the --package-manager CLI flag. Combined with local-tarball - # overrides in CI, that makes non-pnpm paths resolve - # @voidzero-dev/vite-plus-core@0.0.0 from the npm registry and - # fail. Scope this matrix entry to pnpm until the remote-template - # install-flag propagation is sorted out separately. - exclude: - - template: - name: remote-vite-react-ts - package-manager: npm - - template: - name: remote-vite-react-ts - package-manager: yarn - - template: - name: remote-vite-react-ts - package-manager: bun env: VP_OVERRIDE_PACKAGES: '{"vite":"file:${{ github.workspace }}/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz","vitest":"file:${{ github.workspace }}/tmp/tgz/voidzero-dev-vite-plus-test-0.0.0.tgz","@voidzero-dev/vite-plus-core":"file:${{ github.workspace }}/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz","@voidzero-dev/vite-plus-test":"file:${{ github.workspace }}/tmp/tgz/voidzero-dev-vite-plus-test-0.0.0.tgz"}' VP_VERSION: 'file:${{ github.workspace }}/tmp/tgz/vite-plus-0.0.0.tgz' diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index e8b2355d89..18b9b83655 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import { styleText } from 'node:util'; @@ -17,6 +18,7 @@ import { rewriteMonorepo, rewriteMonorepoProject, rewriteStandaloneProject, + setPackageManager, } from '../migration/migrator.ts'; import { DependencyType, PackageManager, type WorkspaceInfo } from '../types/index.ts'; import { @@ -989,6 +991,20 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h updateWorkspaceConfig(projectDir, workspaceInfo); if (shouldMigrateLintFmtTools) { + // Pin the packageManager field before install so `vp install` honors + // the user's `--package-manager` choice instead of falling back to + // pnpm (the hardcoded non-interactive/CI default in vite_install). + setPackageManager(fullPath, workspaceInfo.downloadPackageManager); + // Yarn Berry's default Plug'n'Play stores deps inside .yarn/cache/*.zip, + // which @oxlint/migrate can't resolve (ESM's fileURLToPath over a zip + // entry throws MODULE_NOT_FOUND). Force classic node_modules layout so + // the migration step can import the template's ESLint plugins. + if (workspaceInfo.packageManager === PackageManager.yarn) { + const yarnrcPath = path.join(fullPath, '.yarnrc.yml'); + if (!fs.existsSync(yarnrcPath)) { + fs.writeFileSync(yarnrcPath, 'nodeLinker: node-modules\n'); + } + } updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall( workspaceInfo.rootDir, @@ -1015,6 +1031,20 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h }); } else { if (shouldMigrateLintFmtTools) { + // Pin the packageManager field before install so `vp install` honors + // the user's `--package-manager` choice instead of falling back to + // pnpm (the hardcoded non-interactive/CI default in vite_install). + setPackageManager(fullPath, workspaceInfo.downloadPackageManager); + // Yarn Berry's default Plug'n'Play stores deps inside .yarn/cache/*.zip, + // which @oxlint/migrate can't resolve (ESM's fileURLToPath over a zip + // entry throws MODULE_NOT_FOUND). Force classic node_modules layout so + // the migration step can import the template's ESLint plugins. + if (workspaceInfo.packageManager === PackageManager.yarn) { + const yarnrcPath = path.join(fullPath, '.yarnrc.yml'); + if (!fs.existsSync(yarnrcPath)) { + fs.writeFileSync(yarnrcPath, 'nodeLinker: node-modules\n'); + } + } updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { silent: compactOutput, diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts index 510f40bde2..be37e0442d 100644 --- a/packages/cli/src/migration/migrator.ts +++ b/packages/cli/src/migration/migrator.ts @@ -2367,7 +2367,7 @@ export function rewritePrepareScript(rootDir: string): string | undefined { return oldDir; } -function setPackageManager( +export function setPackageManager( projectDir: string, downloadPackageManager: DownloadPackageManagerResult, ) { From c96de7eded707636f5b0b991a7a139670d4f876b Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 22 Apr 2026 11:29:36 +0800 Subject: [PATCH 10/11] refactor(cli/create): collapse install-and-migrate into single helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify follow-up: the standalone and monorepo branches in vp create were each carrying the same 7-step block (setPackageManager → yarn .yarnrc.yml → progress update → runViteInstall → migrate prompts), differing only in the install cwd. The yarn caveat comment was duplicated verbatim in both blocks. Fold both blocks plus the previously separate `migrateLintFmtTools` arrow into a single `installAndMigrate(installCwd)` closure. The WHY comments live once above the helper. Net -26 lines in create/bin.ts with no behavior change; all four package managers (pnpm/npm/yarn/bun) still migrate `vite@9.0.5 --template react-ts` correctly end-to-end. --- packages/cli/src/create/bin.ts | 90 ++++++++++++---------------------- 1 file changed, 32 insertions(+), 58 deletions(-) diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index 18b9b83655..b51a92b549 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -899,23 +899,6 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h }); resumeCreateProgress(); - // Auto-migrate ESLint → Oxlint and Prettier → Oxfmt after install #1 so - // `@oxlint/migrate` can resolve imports from eslint.config.js, but before - // the vite-plus rewrite so the generated .oxlintrc/.oxfmtrc get merged - // into vite.config.ts — matching `vp migrate`. Always non-interactive; - // Vite+ is opinionated about oxlint/oxfmt. Gated on install success so - // VP_SKIP_INSTALL snap tests don't hit ERR_MODULE_NOT_FOUND. - const migrateLintFmtTools = async (installed: boolean) => { - if (!installed) { - return; - } - updateCreateProgress('Migrating lint and format tools'); - pauseCreateProgress(); - await promptEslintMigration(fullPath, /* interactive */ false); - await promptPrettierMigration(fullPath, /* interactive */ false); - resumeCreateProgress(); - }; - // The migrate-before-rewrite reorder is only needed when the template // actually ships ESLint or Prettier (e.g. `create-vite --template // react-ts`). Builtin templates (vite:library, vite:application, @@ -926,6 +909,36 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h detectEslintProject(fullPath).hasDependency || detectPrettierProject(fullPath).hasDependency; let installSummary: CommandRunSummary | undefined; + + // For templates that ship ESLint/Prettier, install template deps first so + // `@oxlint/migrate` can resolve eslint.config.js's plugin imports, then + // migrate before the vite-plus rewrite so the generated .oxlintrc/.oxfmtrc + // get merged into vite.config.ts — matching `vp migrate`. Pin the + // packageManager field (vite_install hardcodes pnpm in CI/non-TTY when no + // signal is present) and force yarn's classic node_modules layout (PnP + // zip entries break @oxlint/migrate's fileURLToPath resolution). + const installAndMigrate = async (installCwd: string) => { + setPackageManager(fullPath, workspaceInfo.downloadPackageManager); + if (workspaceInfo.packageManager === PackageManager.yarn) { + const yarnrcPath = path.join(fullPath, '.yarnrc.yml'); + if (!fs.existsSync(yarnrcPath)) { + fs.writeFileSync(yarnrcPath, 'nodeLinker: node-modules\n'); + } + } + updateCreateProgress('Installing dependencies'); + installSummary = await runViteInstall(installCwd, options.interactive, installArgs, { + silent: compactOutput, + }); + if (installSummary.status !== 'installed') { + return; + } + updateCreateProgress('Migrating lint and format tools'); + pauseCreateProgress(); + await promptEslintMigration(fullPath, /* interactive */ false); + await promptPrettierMigration(fullPath, /* interactive */ false); + resumeCreateProgress(); + }; + if (isMonorepo) { if (!compactOutput) { prompts.log.step('Monorepo integration...'); @@ -991,28 +1004,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h updateWorkspaceConfig(projectDir, workspaceInfo); if (shouldMigrateLintFmtTools) { - // Pin the packageManager field before install so `vp install` honors - // the user's `--package-manager` choice instead of falling back to - // pnpm (the hardcoded non-interactive/CI default in vite_install). - setPackageManager(fullPath, workspaceInfo.downloadPackageManager); - // Yarn Berry's default Plug'n'Play stores deps inside .yarn/cache/*.zip, - // which @oxlint/migrate can't resolve (ESM's fileURLToPath over a zip - // entry throws MODULE_NOT_FOUND). Force classic node_modules layout so - // the migration step can import the template's ESLint plugins. - if (workspaceInfo.packageManager === PackageManager.yarn) { - const yarnrcPath = path.join(fullPath, '.yarnrc.yml'); - if (!fs.existsSync(yarnrcPath)) { - fs.writeFileSync(yarnrcPath, 'nodeLinker: node-modules\n'); - } - } - updateCreateProgress('Installing dependencies'); - installSummary = await runViteInstall( - workspaceInfo.rootDir, - options.interactive, - installArgs, - { silent: compactOutput }, - ); - await migrateLintFmtTools(installSummary.status === 'installed'); + await installAndMigrate(workspaceInfo.rootDir); } updateCreateProgress('Integrating into monorepo'); rewriteMonorepoProject(fullPath, workspaceInfo.packageManager, undefined, compactOutput); @@ -1031,25 +1023,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h }); } else { if (shouldMigrateLintFmtTools) { - // Pin the packageManager field before install so `vp install` honors - // the user's `--package-manager` choice instead of falling back to - // pnpm (the hardcoded non-interactive/CI default in vite_install). - setPackageManager(fullPath, workspaceInfo.downloadPackageManager); - // Yarn Berry's default Plug'n'Play stores deps inside .yarn/cache/*.zip, - // which @oxlint/migrate can't resolve (ESM's fileURLToPath over a zip - // entry throws MODULE_NOT_FOUND). Force classic node_modules layout so - // the migration step can import the template's ESLint plugins. - if (workspaceInfo.packageManager === PackageManager.yarn) { - const yarnrcPath = path.join(fullPath, '.yarnrc.yml'); - if (!fs.existsSync(yarnrcPath)) { - fs.writeFileSync(yarnrcPath, 'nodeLinker: node-modules\n'); - } - } - updateCreateProgress('Installing dependencies'); - installSummary = await runViteInstall(fullPath, options.interactive, installArgs, { - silent: compactOutput, - }); - await migrateLintFmtTools(installSummary.status === 'installed'); + await installAndMigrate(fullPath); } updateCreateProgress('Applying Vite+ project setup'); rewriteStandaloneProject(fullPath, workspaceInfo, undefined, compactOutput); From 38832300730a6f83bd1419d23ee260fe52b57e32 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 22 Apr 2026 12:04:48 +0800 Subject: [PATCH 11/11] fix(cli/create): spell out Plug'n'Play to satisfy typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI lint failure: crate-ci/typos flagged `PnP` as typo for `On`. The abbreviation appears once in a comment explaining why we force yarn to use classic node_modules. Spell it out as `Plug'n'Play` instead of adding it to the typos allowlist — the full term is clearer anyway. --- packages/cli/src/create/bin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index b51a92b549..059c3358bb 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -915,8 +915,8 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h // migrate before the vite-plus rewrite so the generated .oxlintrc/.oxfmtrc // get merged into vite.config.ts — matching `vp migrate`. Pin the // packageManager field (vite_install hardcodes pnpm in CI/non-TTY when no - // signal is present) and force yarn's classic node_modules layout (PnP - // zip entries break @oxlint/migrate's fileURLToPath resolution). + // signal is present) and force yarn's classic node_modules layout + // (Plug'n'Play zip entries break @oxlint/migrate's fileURLToPath resolution). const installAndMigrate = async (installCwd: string) => { setPackageManager(fullPath, workspaceInfo.downloadPackageManager); if (workspaceInfo.packageManager === PackageManager.yarn) {