diff --git a/packages/cli/src/migration/__tests__/detector.spec.ts b/packages/cli/src/migration/__tests__/detector.spec.ts new file mode 100644 index 0000000000..8f642f7154 --- /dev/null +++ b/packages/cli/src/migration/__tests__/detector.spec.ts @@ -0,0 +1,80 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { detectMigrationWarnings } from '../detector.ts'; + +function writeProject(projectPath: string, packageJson: object, configFiles: string[] = []): void { + fs.mkdirSync(projectPath, { recursive: true }); + fs.writeFileSync(path.join(projectPath, 'package.json'), JSON.stringify(packageJson)); + for (const configFile of configFiles) { + fs.writeFileSync(path.join(projectPath, configFile), ''); + } +} + +describe('detectMigrationWarnings', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-migration-detector-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('detects matching packages and config file variants', () => { + writeProject( + tmpDir, + { + devDependencies: { rollup: '^4.0.0', jest: '^30.0.0' }, + dependencies: { rolldown: '^1.0.0' }, + }, + ['rollup.config.mjs', 'rolldown.config.cjs', 'jest.config.json'], + ); + + expect(detectMigrationWarnings(tmpDir)).toEqual([ + 'Rollup configuration detected. Rollup is API-compatible with Rolldown and can be migrated first before migrating further to tsdown or Vite. Once migrated to tsdown or Vite, re-run vp migrate.', + 'Rolldown configuration detected. Rolldown can be migrated to tsdown or Vite, and it is recommended to do so. Once migrated, re-run vp migrate.', + 'Jest configuration detected. Vitest is highly compatible with Jest, and so we recommend migrating. See the Vitest Jest migration guide: https://vitest.dev/guide/migration/jest, then re-run vp migrate.', + ]); + }); + + it('does not warn when the package and config are not both present', () => { + writeProject(tmpDir, { devDependencies: { rollup: '^4.0.0' } }, ['jest.config.ts']); + + expect(detectMigrationWarnings(tmpDir)).toEqual([]); + }); + + it('detects Jest configuration in package.json', () => { + writeProject(tmpDir, { + devDependencies: { jest: '^30.0.0' }, + jest: { testEnvironment: 'node' }, + }); + + expect(detectMigrationWarnings(tmpDir)).toEqual([ + 'Jest configuration detected. Vitest is highly compatible with Jest, and so we recommend migrating. See the Vitest Jest migration guide: https://vitest.dev/guide/migration/jest, then re-run vp migrate.', + ]); + }); + + it('detects matching packages and configs in workspace packages', () => { + writeProject(path.join(tmpDir, 'packages/app'), { dependencies: { jest: '^30.0.0' } }, [ + 'jest.config.ts', + ]); + + expect(detectMigrationWarnings(tmpDir, [{ name: 'app', path: 'packages/app' }])).toEqual([ + 'Jest configuration detected. Vitest is highly compatible with Jest, and so we recommend migrating. See the Vitest Jest migration guide: https://vitest.dev/guide/migration/jest, then re-run vp migrate.', + ]); + }); + + it('detects workspace configs with dependencies declared at the root', () => { + writeProject(tmpDir, { devDependencies: { jest: '^30.0.0' } }); + writeProject(path.join(tmpDir, 'packages/app'), {}, ['jest.config.js']); + + expect(detectMigrationWarnings(tmpDir, [{ name: 'app', path: 'packages/app' }])).toEqual([ + 'Jest configuration detected. Vitest is highly compatible with Jest, and so we recommend migrating. See the Vitest Jest migration guide: https://vitest.dev/guide/migration/jest, then re-run vp migrate.', + ]); + }); +}); diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index 29cc62b1a2..105068d213 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -39,6 +39,7 @@ import { import type { PackageDependencies } from '../utils/types.ts'; import { detectWorkspace } from '../utils/workspace.ts'; import { checkRolldownCompatibility } from './compat/runner.ts'; +import { detectMigrationWarnings } from './detector.ts'; import { canFormatWithOxfmt, collectChangedFormatPaths, formatMigratedProject } from './format.ts'; import { handleInstallResult } from './install-result.ts'; import { @@ -614,12 +615,7 @@ function showMigrationSummary(options: { if (suggestFullMigration) { log(FULL_MIGRATION_HINT); } - if (report.warnings.length > 0) { - log(`${styleText('yellow', '!')} Warnings:`); - for (const warning of report.warnings) { - log(` - ${warning}`); - } - } + showMigrationWarnings(report.warnings); if (report.manualSteps.length > 0) { log(`${styleText('blue', '→')} Manual follow-up:`); for (const step of report.manualSteps) { @@ -628,6 +624,16 @@ function showMigrationSummary(options: { } } +function showMigrationWarnings(warnings: readonly string[]): void { + if (warnings.length === 0) { + return; + } + log(`${styleText('yellow', '!')} Warnings:`); + for (const warning of warnings) { + log(` - ${warning}`); + } +} + async function downloadSupportedPackageManager(options: { rootDir: string; packageManager: PackageManager; @@ -718,6 +724,9 @@ async function executeMigrationPlan( report: MigrationReport; }> { const report = createMigrationReport(); + report.warnings.push( + ...detectMigrationWarnings(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages), + ); const migrationProgress = interactive ? prompts.spinner({ indicator: 'timer' }) : undefined; let migrationProgressStarted = false; const updateMigrationProgress = (message: string) => { @@ -1062,6 +1071,10 @@ async function main() { let finalInstallOk = true; let canFormatMigratedProject = !process.env.VP_SKIP_INSTALL; const report = createMigrationReport(); + // Only --full opts existing projects into tool migration advice. + const migrationWarnings = options.full + ? detectMigrationWarnings(workspaceInfoOptional.rootDir, workspaceInfoOptional.packages) + : []; // Report reviews at the end with updated locations. Keep them out of the // report until then so review-only items do not force an up-to-date project // through dependency reconciliation on every run. @@ -1227,6 +1240,7 @@ async function main() { ? hasExistingVitePlusMigrationCandidates(workspaceInfoOptional, options) : hasExplicitExistingVitePlusSetupRequest(options)) ) { + showMigrationWarnings(migrationWarnings); if (vitestV5Preflight) { prompts.log.warn(vitestV5Preflight); } @@ -1239,6 +1253,7 @@ async function main() { prompts.outro(`This project is already using Vite+! ${accent('Happy coding!')}`); return; } + report.warnings.push(...migrationWarnings); for (const warning of coreMigrationResult.taskCacheWarnings) { addMigrationWarning(report, warning); } diff --git a/packages/cli/src/migration/detector.ts b/packages/cli/src/migration/detector.ts index aa8bcf4513..cdf40a7934 100644 --- a/packages/cli/src/migration/detector.ts +++ b/packages/cli/src/migration/detector.ts @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import type { WorkspacePackage } from '../types/index.ts'; import { VITE_CONFIG_FILES } from '../utils/constants.ts'; export interface ConfigFiles { @@ -59,6 +60,93 @@ export const PRETTIER_CONFIG_FILES = [ 'prettier.config.mts', ] as const; +const CONFIG_SCRIPT_EXTENSIONS = ['js', 'mjs', 'cjs', 'ts', 'mts', 'cts'] as const; + +const MIGRATION_WARNING_DETECTIONS = [ + { + packageName: 'rollup', + configFiles: CONFIG_SCRIPT_EXTENSIONS.map((extension) => `rollup.config.${extension}`), + warning: + 'Rollup configuration detected. Rollup is API-compatible with Rolldown and can be migrated first before migrating further to tsdown or Vite. Once migrated to tsdown or Vite, re-run vp migrate.', + }, + { + packageName: 'rolldown', + configFiles: CONFIG_SCRIPT_EXTENSIONS.map((extension) => `rolldown.config.${extension}`), + warning: + 'Rolldown configuration detected. Rolldown can be migrated to tsdown or Vite, and it is recommended to do so. Once migrated, re-run vp migrate.', + }, + { + packageName: 'jest', + configFiles: [ + ...CONFIG_SCRIPT_EXTENSIONS.map((extension) => `jest.config.${extension}`), + 'jest.config.json', + ], + packageJsonField: 'jest', + warning: + 'Jest configuration detected. Vitest is highly compatible with Jest, and so we recommend migrating. See the Vitest Jest migration guide: https://vitest.dev/guide/migration/jest, then re-run vp migrate.', + }, +] as const; + +function hasDependency(packageJson: Record, packageName: string): boolean { + return ['dependencies', 'devDependencies', 'optionalDependencies'].some((field) => { + const dependencies = packageJson[field]; + return typeof dependencies === 'object' && dependencies !== null && packageName in dependencies; + }); +} + +/** + * Detect tools that have a direct migration path worth showing before the + * regular Vite+ migration steps run. + */ +export function detectMigrationWarnings( + projectPath: string, + packages: readonly WorkspacePackage[] = [], +): string[] { + const projectPaths = [ + projectPath, + ...packages.map((workspacePackage) => path.join(projectPath, workspacePackage.path)), + ]; + const detected = new Set(); + let rootPackageJson: Record | undefined; + + for (const currentPath of projectPaths) { + const packageJsonPath = path.join(currentPath, 'package.json'); + if (!fs.existsSync(packageJsonPath)) { + continue; + } + + let packageJson: Record; + try { + packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as Record; + } catch { + continue; + } + if (currentPath === projectPath) { + rootPackageJson = packageJson; + } + + for (const detection of MIGRATION_WARNING_DETECTIONS) { + if ( + (hasDependency(packageJson, detection.packageName) || + (currentPath !== projectPath && + rootPackageJson !== undefined && + hasDependency(rootPackageJson, detection.packageName))) && + (detection.configFiles.some((configFile) => + fs.existsSync(path.join(currentPath, configFile)), + ) || + ('packageJsonField' in detection && + Object.hasOwn(packageJson, detection.packageJsonField))) + ) { + detected.add(detection.warning); + } + } + } + + return MIGRATION_WARNING_DETECTIONS.map(({ warning }) => warning).filter((warning) => + detected.has(warning), + ); +} + export function detectConfigs(projectPath: string): ConfigFiles { const configs: ConfigFiles = {};