UNPKG

@entro314labs/at3-toolkit

Version:

Advanced development toolkit for AT3 Stack projects

268 lines (267 loc) 10.8 kB
import { exec } from "child_process"; import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { glob } from "glob"; import { dirname, join, relative } from "path"; import { promisify } from "util"; import { ProjectDetector } from "../detection/detector.js"; import { ConfigMerger } from "./config-merger.js"; const execAsync = promisify(exec); export class MigrationRunner { logger; detector; configMerger; constructor(logger) { this.logger = logger; this.detector = new ProjectDetector(logger); this.configMerger = new ConfigMerger(logger); } async migrate(options) { const startTime = Date.now(); this.logger.step(1, 6, "Analyzing project..."); try { // 1. Detect project const projectInfo = await this.detector.detectProject(options.projectPath); // 2. Create migration plan this.logger.step(2, 6, "Creating migration plan..."); const plan = await this.createMigrationPlan(projectInfo, options); // 3. Create backup this.logger.step(3, 6, "Creating backup..."); const backupInfo = await this.createBackup(options.projectPath, options.backupDir); // 4. Execute migration steps this.logger.step(4, 6, "Executing migration..."); const stepResults = await this.executeMigrationSteps(plan.steps, options); // 5. Update dependencies if (!options.skipDeps) { this.logger.step(5, 6, "Updating dependencies..."); await this.updateDependencies(options, projectInfo); } // 6. Validate migration this.logger.step(6, 6, "Validating migration..."); const validationErrors = await this.validateMigration(options.projectPath); const result = { success: validationErrors.length === 0, steps: stepResults, backupPath: backupInfo.timestamp, errors: validationErrors, warnings: [], }; const duration = Date.now() - startTime; this.logger.success(`Migration completed in ${duration}ms`); return result; } catch (error) { this.logger.error("Migration failed", error); throw error; } } async rollback(projectPath, force = false) { this.logger.info("Starting rollback process..."); const backupDir = join(projectPath, ".migration-backup"); if (!existsSync(backupDir)) { throw new Error("No backup found to rollback from"); } if (!force) { // Interactive confirmation would go here this.logger.warn("This will restore all files from the backup. Continue? (Not implemented in this version)"); } // Restore files from backup const files = await glob("**/*", { cwd: backupDir }); for (const file of files) { const backupFile = join(backupDir, file); const targetFile = join(projectPath, file); if (existsSync(backupFile)) { // Ensure target directory exists mkdirSync(dirname(targetFile), { recursive: true }); copyFileSync(backupFile, targetFile); this.logger.debug(`Restored: ${file}`); } } this.logger.success("Rollback completed successfully"); } async createMigrationPlan(projectInfo, options) { const steps = []; // Add configuration migration steps if (projectInfo.hasNextJs) { steps.push({ id: "next-config", name: "Update Next.js Configuration", description: "Migrate to Next.js 15.4 with modern features", required: true, execute: async () => this.migrateNextConfig(options.projectPath), }); } if (projectInfo.hasTailwind || options.updateVersions) { steps.push({ id: "tailwind-config", name: "Update Tailwind CSS Configuration", description: "Migrate to Tailwind CSS 4.x CSS-first configuration", required: true, execute: async () => this.migrateTailwindConfig(options.projectPath), }); } if (options.replaceLinting && (projectInfo.hasEslint || projectInfo.hasPrettier)) { steps.push({ id: "linting-config", name: "Replace ESLint/Prettier with Biome", description: "Modern unified linting and formatting", required: false, execute: async () => this.migrateLintingConfig(options.projectPath), }); } if (projectInfo.hasTypeScript) { steps.push({ id: "typescript-config", name: "Update TypeScript Configuration", description: "Modern TypeScript 5.9+ configuration", required: true, execute: async () => this.migrateTypeScriptConfig(options.projectPath), }); } return { steps, conflicts: [], backupFiles: [], }; } async createBackup(projectPath, customBackupDir) { const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const backupDir = join(projectPath, customBackupDir || ".migration-backup", timestamp); mkdirSync(backupDir, { recursive: true }); // Files to backup const filesToBackup = [ "package.json", "next.config.*", "tailwind.config.*", "tsconfig.json", "biome.json", ".eslintrc.*", ".prettierrc*", "postcss.config.*", "src/app/globals.css", ]; const backedUpFiles = []; for (const pattern of filesToBackup) { const files = await glob(pattern, { cwd: projectPath }); for (const file of files) { const sourcePath = join(projectPath, file); const backupPath = join(backupDir, file); if (existsSync(sourcePath)) { mkdirSync(dirname(backupPath), { recursive: true }); copyFileSync(sourcePath, backupPath); backedUpFiles.push(file); this.logger.debug(`Backed up: ${file}`); } } } // Create backup info file const backupInfo = { timestamp, files: backedUpFiles, migrationId: `migration-${timestamp}`, canRollback: true, }; writeFileSync(join(backupDir, "backup-info.json"), JSON.stringify(backupInfo, null, 2)); this.logger.success(`Backup created: ${relative(projectPath, backupDir)}`); return backupInfo; } async executeMigrationSteps(steps, options) { const results = []; for (const [index, step] of steps.entries()) { const spinner = this.logger.spinner(`Executing: ${step.name}`); const startTime = Date.now(); try { if (!options.dryRun) { await step.execute(options); } const duration = Date.now() - startTime; spinner.succeed(`✓ ${step.name}`); results.push({ stepId: step.id, success: true, filesModified: [], // Would need to track this in real implementation duration, }); } catch (error) { const duration = Date.now() - startTime; spinner.fail(`✗ ${step.name}`); results.push({ stepId: step.id, success: false, error: error instanceof Error ? error.message : String(error), filesModified: [], duration, }); if (step.required) { throw error; } } } return results; } async updateDependencies(options, projectInfo) { if (options.dryRun) { this.logger.info("Dry run: Skipping dependency installation"); return; } const spinner = this.logger.spinner("Installing dependencies..."); try { const packageManager = projectInfo.packageManager; const installCommand = this.getInstallCommand(packageManager); await execAsync(installCommand, { cwd: options.projectPath, timeout: 300000, // 5 minute timeout }); spinner.succeed("Dependencies updated successfully"); } catch (error) { spinner.fail("Failed to update dependencies"); this.logger.warn("You may need to run the package manager install command manually"); } } async validateMigration(projectPath) { const errors = []; // Check if key files exist and are valid const packageJsonPath = join(projectPath, "package.json"); try { const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); // Validate package.json structure } catch (error) { errors.push({ step: "validation", message: "Invalid package.json after migration", severity: "error", }); } return errors; } async migrateNextConfig(projectPath) { // Implementation for Next.js config migration this.logger.debug("Migrating Next.js configuration"); } async migrateTailwindConfig(projectPath) { // Implementation for Tailwind CSS migration this.logger.debug("Migrating Tailwind CSS configuration"); } async migrateLintingConfig(projectPath) { // Implementation for linting config migration this.logger.debug("Migrating linting configuration"); } async migrateTypeScriptConfig(projectPath) { // Implementation for TypeScript config migration this.logger.debug("Migrating TypeScript configuration"); } getInstallCommand(packageManager) { switch (packageManager) { case "pnpm": return "pnpm install"; case "yarn": return "yarn install"; case "bun": return "bun install"; default: return "npm install"; } } }