Skip to content
80 changes: 80 additions & 0 deletions packages/cli/src/migration/__tests__/detector.spec.ts
Original file line number Diff line number Diff line change
@@ -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.',
]);
});
});
27 changes: 21 additions & 6 deletions packages/cli/src/migration/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1227,6 +1240,7 @@ async function main() {
? hasExistingVitePlusMigrationCandidates(workspaceInfoOptional, options)
: hasExplicitExistingVitePlusSetupRequest(options))
) {
showMigrationWarnings(migrationWarnings);
if (vitestV5Preflight) {
prompts.log.warn(vitestV5Preflight);
}
Expand All @@ -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);
}
Expand Down
88 changes: 88 additions & 0 deletions packages/cli/src/migration/detector.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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;
Comment thread
KTrain5169 marked this conversation as resolved.
Resolved

function hasDependency(packageJson: Record<string, unknown>, 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<string>();
let rootPackageJson: Record<string, unknown> | undefined;

for (const currentPath of projectPaths) {
const packageJsonPath = path.join(currentPath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
continue;
}

let packageJson: Record<string, unknown>;
try {
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as Record<string, unknown>;
} 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 = {};

Expand Down
Loading