From de81a37421cdf1a06bd1890c783a09ec2837f483 Mon Sep 17 00:00:00 2001 From: sakana <185575200+SaKaNa-Y@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:04:10 +0800 Subject: [PATCH 1/2] fix(migrate): finish leftover Oxc config migration Existing Vite+ projects skip standalone Oxc config merging when migration is retried. Reuse the config merger during core finalization for the root and workspace packages, and report completed merges as migration work. Add regression coverage for retry behavior, inline config precedence, merge warnings, and formatting with migrated options. --- .../.oxfmtrc.json | 4 + .../package.json | 9 ++ .../snapshots.toml | 15 +++ .../migration_existing_oxc_configs.md | 91 +++++++++++++++++++ .../src/index.ts | 1 + .../vite.config.ts | 1 + .../src/migration/__tests__/migrator.spec.ts | 91 +++++++++++++++++++ packages/cli/src/migration/bin.ts | 3 +- .../migration/migrator/core-finalization.ts | 32 ++++++- 9 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/.oxfmtrc.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots/migration_existing_oxc_configs.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/src/index.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/vite.config.ts diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/.oxfmtrc.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/.oxfmtrc.json new file mode 100644 index 0000000000..fd496a820e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/.oxfmtrc.json @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "semi": false +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/package.json new file mode 100644 index 0000000000..cdaf891df3 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/package.json @@ -0,0 +1,9 @@ +{ + "name": "migration-existing-oxc-configs", + "private": true, + "type": "module", + "packageManager": "pnpm@12.3.4", + "devDependencies": { + "vite-plus": "latest" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots.toml new file mode 100644 index 0000000000..3923a83db1 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots.toml @@ -0,0 +1,15 @@ +[[case]] +name = "migration_existing_oxc_configs" +vp = "global" +steps = [ + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "finish a leftover Oxfmt config even when Vite+ is already installed" }, + ["vpt", "print-file", "vite.config.ts"], + ["vpt", "stat-file", ".oxfmtrc.json", "--assert-not", "file"], + { argv = ["vp", "fmt", "src/index.ts"], comment = "the migrated options must affect formatting" }, + ["vpt", "print-file", "src/index.ts"], + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "a completed migration should be a no-op on retry" }, + ["vpt", "print-file", "vite.config.ts"], + ["vpt", "stat-file", "AGENTS.md", "--assert-not", "file"], + ["vpt", "stat-file", ".vite-hooks", "--assert-not", "dir"], + ["vpt", "stat-file", ".vscode", "--assert-not", "dir"], +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots/migration_existing_oxc_configs.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots/migration_existing_oxc_configs.md new file mode 100644 index 0000000000..d124881fe9 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots/migration_existing_oxc_configs.md @@ -0,0 +1,91 @@ +# migration_existing_oxc_configs + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +finish a leftover Oxfmt config even when Vite+ is already installed + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite-plus latest → + vite → +• 1 config update applied +• Package manager settings configured +``` + +## `vpt print-file vite.config.ts` + +``` +export default { + fmt: { + "singleQuote": true, + "semi": false + }, + +} +``` + +## `vpt stat-file .oxfmtrc.json --assert-not file` + +``` +.oxfmtrc.json: missing +``` + +## `vp fmt src/index.ts` + +the migrated options must affect formatting + +``` +VITE+ - The Unified Toolchain for the Web + +Finished in on 1 files using threads. +``` + +## `vpt print-file src/index.ts` + +``` +export const message = 'preserved' +``` + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +a completed migration should be a no-op on retry + +``` +VITE+ - The Unified Toolchain for the Web + +This project is already using Vite+! Happy coding! +``` + +## `vpt print-file vite.config.ts` + +``` +export default { + fmt: { + "singleQuote": true, + "semi": false + }, + +} +``` + +## `vpt stat-file AGENTS.md --assert-not file` + +``` +AGENTS.md: missing +``` + +## `vpt stat-file .vite-hooks --assert-not dir` + +``` +.vite-hooks: missing +``` + +## `vpt stat-file .vscode --assert-not dir` + +``` +.vscode: missing +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/src/index.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/src/index.ts new file mode 100644 index 0000000000..38b0fa8a2f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/src/index.ts @@ -0,0 +1 @@ +export const message = "preserved"; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/vite.config.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/vite.config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts index d3e92bdc6c..848eae887d 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -8796,6 +8796,7 @@ describe('existing Vite+ core migration finalization', () => { scripts: true, tsconfigTypes: true, imports: true, + oxcConfigs: false, }); const pkg = readJson(path.join(tmpDir, 'package.json')) as { @@ -8826,6 +8827,96 @@ describe('existing Vite+ core migration finalization', () => { }); }); + it.each(['.oxfmtrc.json', '.oxfmtrc.jsonc'])( + 'finishes a leftover %s config and remains idempotent', + (configFile) => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ name: 'test', devDependencies: { 'vite-plus': 'latest' } }), + ); + fs.writeFileSync(path.join(tmpDir, 'vite.config.ts'), 'export default {};\n'); + fs.writeFileSync(path.join(tmpDir, configFile), '{"singleQuote":true,"semi":false}\n'); + const workspaceInfo = makeWorkspaceInfo(tmpDir, PackageManager.npm); + const report = createMigrationReport(); + + expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true, report).oxcConfigs).toBe( + true, + ); + const config = fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8'); + expect(config).toContain('fmt:'); + expect(config).toContain('"singleQuote":true'); + expect(config).toContain('"semi":false'); + expect(fs.existsSync(path.join(tmpDir, configFile))).toBe(false); + expect(report.mergedConfigCount).toBe(1); + expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true).oxcConfigs).toBe(false); + expect(fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8')).toBe(config); + }, + ); + + it('finishes leftover lint and format configs in workspace packages', () => { + const appDir = path.join(tmpDir, 'packages', 'app'); + fs.mkdirSync(appDir, { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ name: 'root', devDependencies: { 'vite-plus': 'latest' } }), + ); + fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ name: 'app' })); + fs.writeFileSync(path.join(appDir, '.oxfmtrc.json'), '{"singleQuote":true}\n'); + fs.writeFileSync(path.join(appDir, '.oxlintrc.json'), '{"rules":{"no-console":"error"}}\n'); + const workspaceInfo = { + ...makeWorkspaceInfo(tmpDir, PackageManager.pnpm), + isMonorepo: true, + packages: [{ name: 'app', path: 'packages/app' }], + }; + + expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true).oxcConfigs).toBe(true); + const config = fs.readFileSync(path.join(appDir, 'vite.config.ts'), 'utf8'); + expect(config).toContain('fmt:'); + expect(config).toContain('lint:'); + expect(config).toMatch(/"no-console":\s*"error"/); + expect(fs.existsSync(path.join(appDir, '.oxfmtrc.json'))).toBe(false); + expect(fs.existsSync(path.join(appDir, '.oxlintrc.json'))).toBe(false); + expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true).oxcConfigs).toBe(false); + }); + + it('preserves existing inline config when removing a redundant standalone config', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ name: 'test', devDependencies: { 'vite-plus': 'latest' } }), + ); + const config = 'export default { fmt: { singleQuote: false } };\n'; + fs.writeFileSync(path.join(tmpDir, 'vite.config.ts'), config); + fs.writeFileSync(path.join(tmpDir, '.oxfmtrc.json'), '{"singleQuote":true}\n'); + const workspaceInfo = makeWorkspaceInfo(tmpDir, PackageManager.npm); + + expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true).oxcConfigs).toBe(true); + expect(fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8')).toBe(config); + expect(fs.existsSync(path.join(tmpDir, '.oxfmtrc.json'))).toBe(false); + expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true).oxcConfigs).toBe(false); + }); + + it('keeps an unmergeable config and reports the incomplete migration', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ name: 'test', devDependencies: { 'vite-plus': 'latest' } }), + ); + fs.writeFileSync( + path.join(tmpDir, 'vite.config.ts'), + 'const config = {}; export default config;\n', + ); + fs.writeFileSync(path.join(tmpDir, '.oxfmtrc.json'), '{"singleQuote":true}\n'); + const report = createMigrationReport(); + + const result = finalizeCoreMigrationForExistingVitePlus( + makeWorkspaceInfo(tmpDir, PackageManager.npm), + true, + report, + ); + expect(result.oxcConfigs).toBe(false); + expect(fs.existsSync(path.join(tmpDir, '.oxfmtrc.json'))).toBe(true); + expect(report.warnings.some((warning) => warning.includes('Failed to merge'))).toBe(true); + }); + it('detects package-level legacy signals in workspaces', () => { const appDir = path.join(tmpDir, 'packages', 'app'); fs.mkdirSync(appDir, { recursive: true }); diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index b43647875e..a7e0c0c5a1 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -1114,7 +1114,8 @@ async function main() { if ( coreMigrationResult.scripts || coreMigrationResult.tsconfigTypes || - coreMigrationResult.imports + coreMigrationResult.imports || + coreMigrationResult.oxcConfigs ) { didMigrate = true; } diff --git a/packages/cli/src/migration/migrator/core-finalization.ts b/packages/cli/src/migration/migrator/core-finalization.ts index 29af68a91a..4e25bc7011 100644 --- a/packages/cli/src/migration/migrator/core-finalization.ts +++ b/packages/cli/src/migration/migrator/core-finalization.ts @@ -5,7 +5,13 @@ import { rewriteScripts } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; import { editJsonFile, readJsonFile } from '../../utils/json.ts'; import { rulesDir } from '../../utils/path.ts'; -import { hasTsconfigTypesToRewrite, rewriteAllImports, rewriteTsconfigTypes } from '../migrator.ts'; +import { detectConfigs } from '../detector.ts'; +import { + hasTsconfigTypesToRewrite, + mergeViteConfigFiles, + rewriteAllImports, + rewriteTsconfigTypes, +} from '../migrator.ts'; import { type MigrationReport } from '../report.ts'; const RULES_YAML_PATH = path.join(rulesDir, 'vite-tools.yml'); @@ -76,6 +82,7 @@ export type CoreMigrationFinalizationResult = { scripts: boolean; tsconfigTypes: boolean; imports: boolean; + oxcConfigs: boolean; }; function getCoreMigrationProjectPaths(workspaceInfo: CoreMigrationWorkspace): string[] { @@ -140,6 +147,7 @@ export function finalizeCoreMigrationForExistingVitePlus( scripts: false, tsconfigTypes: false, imports: false, + oxcConfigs: false, }; if (pending.scripts) { @@ -157,5 +165,27 @@ export function finalizeCoreMigrationForExistingVitePlus( result.imports = rewriteAllImports(workspaceInfo.rootDir, silent, report, true); + // A failed migration may have installed Vite+ before merging these files. + // Finish that core work without opting into unrelated first-time setup. + for (const projectPath of projectPaths) { + const configs = detectConfigs(projectPath); + const standaloneConfigs = [configs.oxlintConfig, configs.oxfmtConfig].filter( + (config) => config !== undefined, + ); + if (standaloneConfigs.length === 0) { + continue; + } + mergeViteConfigFiles( + projectPath, + silent, + report, + workspaceInfo.packages, + workspaceInfo.rootDir, + ); + if (standaloneConfigs.some((config) => !fs.existsSync(path.join(projectPath, config)))) { + result.oxcConfigs = true; + } + } + return result; } From 2311e1482449ab1694f4d549d8ccd0d30520c18a Mon Sep 17 00:00:00 2001 From: Sakana <15715093608@163.com> Date: Fri, 11 Sep 2026 19:49:58 +0800 Subject: [PATCH 2/2] test(migrate): cover existing inline fmt in CLI snapshots Verify that migration removes a leftover Oxfmt config while preserving conflicting inline options and formatting behavior. Check that a second migration is a no-op. Both Oxc snapshot cases pass. The new case fails its config-removal assertion with the pre-fix migration entry. --- .../snapshots.toml | 17 ++++ ...gration_existing_oxc_configs_inline_fmt.md | 84 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots/migration_existing_oxc_configs_inline_fmt.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots.toml index 3923a83db1..eb4e7224f9 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots.toml @@ -13,3 +13,20 @@ steps = [ ["vpt", "stat-file", ".vite-hooks", "--assert-not", "dir"], ["vpt", "stat-file", ".vscode", "--assert-not", "dir"], ] + +[[case]] +name = "migration_existing_oxc_configs_inline_fmt" +vp = "global" +steps = [ + { argv = ["vpt", "write-file", "vite.config.ts", "export default { fmt: { singleQuote: false, semi: false } };\n"], snapshot = false }, + { argv = ["vp", "fmt", "src/index.ts"], comment = "the existing inline fmt config takes precedence over the standalone config" }, + ["vpt", "print-file", "src/index.ts"], + { argv = ["vpt", "write-file", "src/index.ts", "export const message = \"preserved\";\n"], snapshot = false }, + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "remove the leftover standalone config without changing the existing fmt config" }, + ["vpt", "print-file", "vite.config.ts"], + ["vpt", "stat-file", ".oxfmtrc.json", "--assert-not", "file"], + { argv = ["vp", "fmt", "src/index.ts"], comment = "formatting must still use the existing inline options after migration" }, + ["vpt", "print-file", "src/index.ts"], + { argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "retrying the completed migration should be a no-op" }, + ["vpt", "print-file", "vite.config.ts"], +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots/migration_existing_oxc_configs_inline_fmt.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots/migration_existing_oxc_configs_inline_fmt.md new file mode 100644 index 0000000000..5e5db5b33d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots/migration_existing_oxc_configs_inline_fmt.md @@ -0,0 +1,84 @@ +# migration_existing_oxc_configs_inline_fmt + +## `vpt write-file vite.config.ts 'export default { fmt: { singleQuote: false, semi: false } }; +'` + + +## `vp fmt src/index.ts` + +the existing inline fmt config takes precedence over the standalone config + +``` +VITE+ - The Unified Toolchain for the Web + +Finished in on 1 files using threads. +``` + +## `vpt print-file src/index.ts` + +``` +export const message = "preserved" +``` + +## `vpt write-file src/index.ts 'export const message = "preserved"; +'` + + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +remove the leftover standalone config without changing the existing fmt config + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite-plus latest → + vite → +• Package manager settings configured +``` + +## `vpt print-file vite.config.ts` + +``` +export default { fmt: { singleQuote: false, semi: false } }; +``` + +## `vpt stat-file .oxfmtrc.json --assert-not file` + +``` +.oxfmtrc.json: missing +``` + +## `vp fmt src/index.ts` + +formatting must still use the existing inline options after migration + +``` +VITE+ - The Unified Toolchain for the Web + +Finished in on 1 files using threads. +``` + +## `vpt print-file src/index.ts` + +``` +export const message = "preserved" +``` + +## `vp migrate --no-interactive --no-hooks --no-agent --no-editor` + +retrying the completed migration should be a no-op + +``` +VITE+ - The Unified Toolchain for the Web + +This project is already using Vite+! Happy coding! +``` + +## `vpt print-file vite.config.ts` + +``` +export default { fmt: { singleQuote: false, semi: false } }; +```