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 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..dd2e928386 --- /dev/null +++ b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/snap.txt @@ -0,0 +1,135 @@ +> 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 + +> 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 . +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 new file mode 100644 index 0000000000..e1fde38fa5 --- /dev/null +++ b/packages/cli/snap-tests-global/new-create-vite-migrates-eslint-prettier/steps.json @@ -0,0 +1,16 @@ +{ + "env": { + "VP_SKIP_INSTALL": "", + "CI": "" + }, + "commands": [ + { + "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)", + "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" + ] +} diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index cedf7fdf5e..059c3358bb 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'; @@ -7,12 +8,17 @@ import mri from 'mri'; import { vitePlusHeader } from '../../binding/index.js'; import { addFrameworkShim, + detectEslintProject, detectFramework, + detectPrettierProject, hasFrameworkShim, installGitHooks, + promptEslintMigration, + promptPrettierMigration, rewriteMonorepo, rewriteMonorepoProject, rewriteStandaloneProject, + setPackageManager, } from '../migration/migrator.ts'; import { DependencyType, PackageManager, type WorkspaceInfo } from '../types/index.ts'; import { @@ -893,18 +899,50 @@ 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; + + // 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 + // (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) { + 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...'); } - 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) { @@ -965,6 +1003,16 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h } updateWorkspaceConfig(projectDir, workspaceInfo); + if (shouldMigrateLintFmtTools) { + await installAndMigrate(workspaceInfo.rootDir); + } + updateCreateProgress('Integrating into monorepo'); + rewriteMonorepoProject(fullPath, workspaceInfo.packageManager, undefined, compactOutput); + for (const framework of detectFramework(fullPath)) { + if (!hasFrameworkShim(fullPath, framework)) { + addFrameworkShim(fullPath, framework); + } + } updateCreateProgress('Installing dependencies'); installSummary = await runViteInstall(workspaceInfo.rootDir, options.interactive, installArgs, { silent: compactOutput, @@ -974,6 +1022,9 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h silent: compactOutput, }); } else { + if (shouldMigrateLintFmtTools) { + await installAndMigrate(fullPath); + } updateCreateProgress('Applying Vite+ project setup'); rewriteStandaloneProject(fullPath, workspaceInfo, undefined, compactOutput); for (const framework of detectFramework(fullPath)) { 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..be37e0442d 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, @@ -2366,7 +2367,7 @@ export function rewritePrepareScript(rootDir: string): string | undefined { return oldDir; } -function setPackageManager( +export function setPackageManager( projectDir: string, downloadPackageManager: DownloadPackageManagerResult, ) { @@ -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.', 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.', 1); + } + return true; +} diff --git a/rfcs/code-generator.md b/rfcs/code-generator.md index 5c811f3344..0b7d4a2c66 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: + • 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 + • 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/)**: @@ -977,13 +995,23 @@ 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 @@ -2097,6 +2125,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 +2192,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