Skip to content

Commit c237124

Browse files
committed
feat: add vite migration command
1 parent c109707 commit c237124

9 files changed

Lines changed: 1969 additions & 1 deletion

File tree

packages/global/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
// Parse command line arguments to intercept 'new' and 'gen' commands
1+
// Parse command line arguments to intercept 'new', 'gen', and 'migration' commands
22
const args = process.argv.slice(2);
33

44
const command = args[0];
55
if (command === 'gen' || command === 'g' || command === 'generate' || command === 'new') {
66
import('./gen.ts');
7+
} else if (command === 'migration' || command === 'migrate') {
8+
import('./migrate.ts');
79
} else {
810
// Delegate all other commands to vite-plus CLI
911
import('@voidzero-dev/vite-plus/bin');

packages/global/src/migrate.ts

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import path from 'node:path';
2+
3+
import * as prompts from '@clack/prompts';
4+
import mri from 'mri';
5+
import colors from 'picocolors';
6+
7+
import { detectTools, hasVitePlus } from './migration/detector.ts';
8+
import { performMigration, previewMigration } from './migration/migrator.ts';
9+
import type { MigrationOptions } from './migration/types.ts';
10+
11+
const { blue, cyan, green, gray, red, yellow } = colors;
12+
13+
// prettier-ignore
14+
const helpMessage = `\\\
15+
Usage: vite migration [PATH] [OPTIONS]
16+
17+
Migrate standalone vite, vitest, tsdown, oxlint, and oxfmt to unified vite-plus.
18+
19+
Arguments:
20+
PATH Path to the package to migrate (default: current directory)
21+
22+
Options:
23+
--dry-run Preview changes without applying them
24+
--no-interactive Run in non-interactive mode (skip prompts and use defaults)
25+
--all Migrate all packages in workspace (monorepo)
26+
--tools Migrate specific tools only (comma-separated: vite,vitest,tsdown,oxlint,oxfmt)
27+
--check Check if migration is needed without applying
28+
-h, --help Show this help message
29+
30+
Examples:
31+
${gray('# Migrate current package')}
32+
vite migration
33+
34+
${gray('# Migrate specific package')}
35+
vite migration packages/my-app
36+
37+
${gray('# Preview changes without applying')}
38+
vite migration --dry-run
39+
40+
${gray('# Migrate specific tools only')}
41+
vite migration --tools=vite,vitest
42+
43+
${gray('# Check if migration is needed')}
44+
vite migration --check
45+
46+
${gray('# Non-interactive mode')}
47+
vite migration --no-interactive
48+
49+
Aliases: ${gray('migrate')}
50+
`;
51+
52+
function parseArgs() {
53+
const args = process.argv.slice(3); // Skip 'node', 'vite', 'migration'
54+
55+
const parsed = mri<{
56+
'dry-run'?: boolean;
57+
all?: boolean;
58+
tools?: string;
59+
check?: boolean;
60+
help?: boolean;
61+
interactive?: boolean;
62+
}>(args, {
63+
alias: { h: 'help' },
64+
boolean: ['help', 'dry-run', 'all', 'check', 'interactive'],
65+
string: ['tools'],
66+
default: { interactive: process.stdin.isTTY },
67+
});
68+
69+
const projectPath = parsed._[0] as string | undefined;
70+
71+
return {
72+
projectPath: projectPath || process.cwd(),
73+
options: {
74+
path: projectPath || process.cwd(),
75+
dryRun: parsed['dry-run'] || false,
76+
all: parsed.all || false,
77+
tools: parsed.tools?.split(',').map(t => t.trim()),
78+
check: parsed.check || false,
79+
interactive: parsed.interactive,
80+
help: parsed.help || false,
81+
} as MigrationOptions,
82+
};
83+
}
84+
85+
async function main() {
86+
const { projectPath, options } = parseArgs();
87+
88+
// Handle help flag
89+
if (options.help) {
90+
console.log(helpMessage);
91+
return;
92+
}
93+
94+
// Start migration
95+
prompts.intro(cyan('Vite+ Migration'));
96+
97+
const spinner = prompts.spinner();
98+
spinner.start('Analyzing project...');
99+
100+
// Check if already using vite-plus
101+
if (hasVitePlus(projectPath)) {
102+
spinner.stop('Analysis complete');
103+
prompts.log.warn('Package already uses vite-plus!');
104+
prompts.log.info('Nothing to migrate.');
105+
if (options.interactive) {
106+
prompts.outro('Done');
107+
}
108+
return;
109+
}
110+
111+
// Detect tools
112+
const detection = await detectTools(projectPath);
113+
spinner.stop('Analysis complete');
114+
115+
// Check if there's anything to migrate
116+
if (!detection.hasVite && !detection.hasVitest && !detection.hasOxlint && !detection.hasOxfmt) {
117+
prompts.log.warn('No standalone tools detected. Nothing to migrate.');
118+
if (options.interactive) {
119+
prompts.outro('Done');
120+
}
121+
return;
122+
}
123+
124+
// Show detected tools
125+
prompts.log.info('Detected standalone tools:');
126+
if (detection.hasVite) prompts.log.success(` vite ${detection.dependencies.vite}`);
127+
if (detection.hasVitest) prompts.log.success(` vitest ${detection.dependencies.vitest}`);
128+
if (detection.hasOxlint) prompts.log.success(` oxlint ${detection.dependencies.oxlint}`);
129+
if (detection.hasOxfmt) prompts.log.success(` oxfmt ${detection.dependencies.oxfmt}`);
130+
131+
// Show configuration files found
132+
const configFiles = Object.values(detection.configs).filter(Boolean);
133+
if (configFiles.length > 0) {
134+
prompts.log.info('\\nConfiguration files found:');
135+
for (const config of configFiles) {
136+
prompts.log.info(` • ${config}`);
137+
}
138+
}
139+
140+
// If --check mode, exit here
141+
if (options.check) {
142+
prompts.log.success('\\nMigration is available for this project.');
143+
if (options.interactive) {
144+
prompts.outro('Run without --check to perform migration');
145+
}
146+
return;
147+
}
148+
149+
// Preview migration plan
150+
const plan = await previewMigration(projectPath);
151+
152+
prompts.log.info('\\nMigration plan:');
153+
154+
if (plan.dependencies.toRemove.length > 0) {
155+
prompts.log.info(' Dependencies (package.json):');
156+
for (const dep of plan.dependencies.toRemove) {
157+
console.log(` ${red('-')} ${dep}`);
158+
}
159+
for (const dep of plan.dependencies.toAdd) {
160+
console.log(` ${green('+')} ${dep}`);
161+
}
162+
}
163+
164+
if (plan.configs.changes.length > 0) {
165+
prompts.log.info('\\n Configuration:');
166+
for (const change of plan.configs.changes) {
167+
prompts.log.success(` ✓ ${change}`);
168+
}
169+
}
170+
171+
if (plan.filesToDelete.length > 0) {
172+
prompts.log.info('\\n Files to remove:');
173+
for (const file of plan.filesToDelete) {
174+
console.log(` ${red('×')} ${file}`);
175+
}
176+
}
177+
178+
// Confirm migration
179+
if (options.interactive && !options.dryRun) {
180+
const shouldProceed = await prompts.confirm({
181+
message: 'Proceed with migration?',
182+
});
183+
184+
if (prompts.isCancel(shouldProceed) || !shouldProceed) {
185+
prompts.cancel('Migration cancelled');
186+
return;
187+
}
188+
}
189+
190+
// Perform migration
191+
spinner.start(options.dryRun ? 'Previewing migrations...' : 'Applying migrations...');
192+
193+
const result = await performMigration(projectPath, options);
194+
195+
if (result.success) {
196+
spinner.stop(options.dryRun ? 'Preview completed!' : 'Migration completed!');
197+
198+
// Show results
199+
prompts.log.success(options.dryRun ? '\\nChanges preview:' : '\\nChanges applied:');
200+
for (const r of result.results) {
201+
if (r.success) {
202+
prompts.log.success(` ✓ ${r.message}`);
203+
} else {
204+
prompts.log.error(` × ${r.message}`);
205+
}
206+
}
207+
208+
if (!options.dryRun) {
209+
// Show next steps
210+
const nextSteps = `
211+
${blue('Next steps:')}
212+
1. Review vite.config.ts to ensure configurations are correct
213+
2. Run ${cyan('vite install')} to update dependencies
214+
3. Run ${cyan('vite build')} and ${cyan('vite test')} to verify everything works
215+
216+
${result.backup ? yellow(`Backup created at: ${result.backup}`) : ''}
217+
${result.backup ? gray('To rollback: restore files from backup directory') : ''}
218+
`.trim();
219+
220+
console.log('\\n' + nextSteps + '\\n');
221+
}
222+
223+
if (options.interactive) {
224+
prompts.outro(green('✨ ' + (options.dryRun ? 'Preview completed!' : 'Migration completed!')));
225+
}
226+
} else {
227+
spinner.stop('Migration failed');
228+
229+
for (const r of result.results) {
230+
if (!r.success) {
231+
prompts.log.error(r.message);
232+
}
233+
}
234+
235+
if (options.interactive) {
236+
prompts.outro(red('Migration failed'));
237+
}
238+
process.exit(1);
239+
}
240+
}
241+
242+
main().catch((err) => {
243+
prompts.log.error(err.message);
244+
console.error(err);
245+
process.exit(1);
246+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
import type { DetectionResult, PackageManager } from './types.ts';
5+
6+
export function detectPackageManager(projectPath: string): PackageManager {
7+
// Check for lock files to detect package manager
8+
if (fs.existsSync(path.join(projectPath, 'pnpm-lock.yaml'))) {
9+
return 'pnpm' as PackageManager;
10+
}
11+
if (fs.existsSync(path.join(projectPath, 'yarn.lock'))) {
12+
return 'yarn' as PackageManager;
13+
}
14+
if (fs.existsSync(path.join(projectPath, 'bun.lockb'))) {
15+
return 'bun' as PackageManager;
16+
}
17+
if (fs.existsSync(path.join(projectPath, 'package-lock.json'))) {
18+
return 'npm' as PackageManager;
19+
}
20+
21+
// Default to npm if no lock file found
22+
return 'npm' as PackageManager;
23+
}
24+
25+
export function migrateDependencies(
26+
projectPath: string,
27+
detection: DetectionResult,
28+
vitePlusVersion = '^0.1.0',
29+
): void {
30+
const pkgJsonPath = path.join(projectPath, 'package.json');
31+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
32+
33+
// Remove standalone tool dependencies
34+
const depsToRemove = ['vite', 'vitest', 'oxlint', 'oxfmt'];
35+
36+
for (const dep of depsToRemove) {
37+
if (pkgJson.dependencies?.[dep]) {
38+
delete pkgJson.dependencies[dep];
39+
}
40+
if (pkgJson.devDependencies?.[dep]) {
41+
delete pkgJson.devDependencies[dep];
42+
}
43+
}
44+
45+
// Add vite-plus
46+
if (!pkgJson.devDependencies) {
47+
pkgJson.devDependencies = {};
48+
}
49+
pkgJson.devDependencies['vite-plus'] = vitePlusVersion;
50+
51+
// Add overrides for vite → vite-plus (package manager specific)
52+
const packageManager = detectPackageManager(projectPath);
53+
54+
if (packageManager === 'yarn') {
55+
// Yarn uses 'resolutions'
56+
if (!pkgJson.resolutions) {
57+
pkgJson.resolutions = {};
58+
}
59+
pkgJson.resolutions.vite = `npm:vite-plus@${vitePlusVersion}`;
60+
} else {
61+
// npm, pnpm, bun use 'overrides'
62+
if (!pkgJson.overrides) {
63+
pkgJson.overrides = {};
64+
}
65+
pkgJson.overrides.vite = `npm:vite-plus@${vitePlusVersion}`;
66+
}
67+
68+
// Write back to package.json (preserve formatting)
69+
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n', 'utf-8');
70+
}

0 commit comments

Comments
 (0)