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..eb4e7224f9 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_oxc_configs/snapshots.toml @@ -0,0 +1,32 @@ +[[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"], +] + +[[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.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/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 } }; +``` 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 c395b594db..026874eb0a 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, tsdownConfig: false, }); @@ -8827,6 +8828,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 }); @@ -8852,6 +8943,40 @@ describe('existing Vite+ core migration finalization', () => { expect(appPkg.scripts.dev).toBe('vp dev'); }); + it('finishes leftover Oxc and tsdown configs together and remains idempotent', () => { + 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, '.oxfmtrc.json'), '{"singleQuote":true,"semi":false}\n'); + fs.writeFileSync( + path.join(tmpDir, 'tsdown.config.ts'), + "import { defineConfig } from 'tsdown'; export default defineConfig({ entry: 'src/index.ts' });\n", + ); + const workspaceInfo = makeWorkspaceInfo(tmpDir, PackageManager.pnpm); + + const result = finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true); + expect(result.oxcConfigs).toBe(true); + expect(result.tsdownConfig).toBe(true); + const config = fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8'); + expect(config).toContain('pack: tsdownConfig'); + expect(config).toContain('"singleQuote":true'); + expect(config).toContain('"semi":false'); + expect(fs.existsSync(path.join(tmpDir, '.oxfmtrc.json'))).toBe(false); + expect(fs.readFileSync(path.join(tmpDir, 'tsdown.config.ts'), 'utf8')).toContain( + "from 'vite-plus/pack'", + ); + expect(finalizeCoreMigrationForExistingVitePlus(workspaceInfo, true)).toEqual({ + scripts: false, + tsconfigTypes: false, + imports: false, + oxcConfigs: false, + tsdownConfig: false, + }); + expect(fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8')).toBe(config); + }); + it('makes a leftover tsdown config discoverable in an existing Vite+ project', () => { fs.writeFileSync( path.join(tmpDir, 'package.json'), @@ -8873,6 +8998,7 @@ export default defineConfig({ tsconfigTypes: false, imports: true, tsdownConfig: true, + oxcConfigs: false, }); expect(fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8')).toContain( "import tsdownConfig from './tsdown.config.js';", @@ -8889,6 +9015,7 @@ export default defineConfig({ tsconfigTypes: false, imports: false, tsdownConfig: false, + oxcConfigs: false, }); }); @@ -8925,6 +9052,7 @@ export default defineConfig({ entry: 'src/index.ts' }); tsconfigTypes: false, imports: true, tsdownConfig: false, + oxcConfigs: false, }); expect(fs.readFileSync(path.join(tmpDir, 'vite.config.ts'), 'utf8')).toBe(originalViteConfig); expect(report.tsdownImportCount).toBe(0); diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index ceaaa61ce8..673df61615 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -1115,6 +1115,7 @@ async function main() { coreMigrationResult.scripts || coreMigrationResult.tsconfigTypes || coreMigrationResult.imports || + coreMigrationResult.oxcConfigs || coreMigrationResult.tsdownConfig ) { didMigrate = true; diff --git a/packages/cli/src/migration/migrator/core-finalization.ts b/packages/cli/src/migration/migrator/core-finalization.ts index 46b0d51d76..ea2467186e 100644 --- a/packages/cli/src/migration/migrator/core-finalization.ts +++ b/packages/cli/src/migration/migrator/core-finalization.ts @@ -5,9 +5,11 @@ 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 { detectConfigs } from '../detector.ts'; import { hasTsconfigTypesToRewrite, mergeTsdownConfigFile, + mergeViteConfigFiles, rewriteAllImports, rewriteTsconfigTypes, } from '../migrator.ts'; @@ -81,6 +83,7 @@ export type CoreMigrationFinalizationResult = { scripts: boolean; tsconfigTypes: boolean; imports: boolean; + oxcConfigs: boolean; tsdownConfig: boolean; }; @@ -146,6 +149,7 @@ export function finalizeCoreMigrationForExistingVitePlus( scripts: false, tsconfigTypes: false, imports: false, + oxcConfigs: false, tsdownConfig: false, }; @@ -171,5 +175,27 @@ export function finalizeCoreMigrationForExistingVitePlus( result.tsdownConfig = mergeTsdownConfigFile(projectPath, silent, report) || result.tsdownConfig; } + // 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; }