317 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			317 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import * as plugins from './tstest.plugins.js';
 | |
| import { coloredString as cs } from '@push.rocks/consolecolor';
 | |
| import { parseTestFilename, getLegacyMigrationTarget, isLegacyFilename } from './tstest.classes.runtime.parser.js';
 | |
| 
 | |
| /**
 | |
|  * Migration result for a single file
 | |
|  */
 | |
| export interface MigrationResult {
 | |
|   /**
 | |
|    * Original file path
 | |
|    */
 | |
|   oldPath: string;
 | |
| 
 | |
|   /**
 | |
|    * New file path after migration
 | |
|    */
 | |
|   newPath: string;
 | |
| 
 | |
|   /**
 | |
|    * Whether the migration was performed
 | |
|    */
 | |
|   migrated: boolean;
 | |
| 
 | |
|   /**
 | |
|    * Error message if migration failed
 | |
|    */
 | |
|   error?: string;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Migration summary
 | |
|  */
 | |
| export interface MigrationSummary {
 | |
|   /**
 | |
|    * Total number of legacy files found
 | |
|    */
 | |
|   totalLegacyFiles: number;
 | |
| 
 | |
|   /**
 | |
|    * Number of files successfully migrated
 | |
|    */
 | |
|   migratedCount: number;
 | |
| 
 | |
|   /**
 | |
|    * Number of files that failed to migrate
 | |
|    */
 | |
|   errorCount: number;
 | |
| 
 | |
|   /**
 | |
|    * Individual migration results
 | |
|    */
 | |
|   results: MigrationResult[];
 | |
| 
 | |
|   /**
 | |
|    * Whether this was a dry run
 | |
|    */
 | |
|   dryRun: boolean;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Migration options
 | |
|  */
 | |
| export interface MigrationOptions {
 | |
|   /**
 | |
|    * Base directory to search for test files
 | |
|    * Default: process.cwd()
 | |
|    */
 | |
|   baseDir?: string;
 | |
| 
 | |
|   /**
 | |
|    * Glob pattern for finding test files
 | |
|    * Default: '** /*test*.ts' (without space)
 | |
|    */
 | |
|   pattern?: string;
 | |
| 
 | |
|   /**
 | |
|    * Dry run mode - don't actually rename files
 | |
|    * Default: true
 | |
|    */
 | |
|   dryRun?: boolean;
 | |
| 
 | |
|   /**
 | |
|    * Verbose output
 | |
|    * Default: false
 | |
|    */
 | |
|   verbose?: boolean;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Migration class for renaming legacy test files to new naming convention
 | |
|  *
 | |
|  * Migrations:
 | |
|  * - .browser.ts → .chromium.ts
 | |
|  * - .both.ts → .node+chromium.ts
 | |
|  * - .both.nonci.ts → .node+chromium.nonci.ts
 | |
|  * - .browser.nonci.ts → .chromium.nonci.ts
 | |
|  */
 | |
| export class Migration {
 | |
|   private options: Required<MigrationOptions>;
 | |
| 
 | |
|   constructor(options: MigrationOptions = {}) {
 | |
|     this.options = {
 | |
|       baseDir: options.baseDir || process.cwd(),
 | |
|       pattern: options.pattern || '**/test*.ts',
 | |
|       dryRun: options.dryRun !== undefined ? options.dryRun : true,
 | |
|       verbose: options.verbose || false,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Find all legacy test files in the base directory
 | |
|    */
 | |
|   async findLegacyFiles(): Promise<string[]> {
 | |
|     const files = await plugins.smartfile.fs.listFileTree(
 | |
|       this.options.baseDir,
 | |
|       this.options.pattern
 | |
|     );
 | |
| 
 | |
|     const legacyFiles: string[] = [];
 | |
| 
 | |
|     for (const file of files) {
 | |
|       const fileName = plugins.path.basename(file);
 | |
|       if (isLegacyFilename(fileName)) {
 | |
|         const absolutePath = plugins.path.isAbsolute(file)
 | |
|           ? file
 | |
|           : plugins.path.join(this.options.baseDir, file);
 | |
|         legacyFiles.push(absolutePath);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return legacyFiles;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Migrate a single file
 | |
|    */
 | |
|   private async migrateFile(filePath: string): Promise<MigrationResult> {
 | |
|     const fileName = plugins.path.basename(filePath);
 | |
|     const dirName = plugins.path.dirname(filePath);
 | |
| 
 | |
|     try {
 | |
|       // Get the new filename
 | |
|       const newFileName = getLegacyMigrationTarget(fileName);
 | |
| 
 | |
|       if (!newFileName) {
 | |
|         return {
 | |
|           oldPath: filePath,
 | |
|           newPath: filePath,
 | |
|           migrated: false,
 | |
|           error: 'File is not a legacy file',
 | |
|         };
 | |
|       }
 | |
| 
 | |
|       const newPath = plugins.path.join(dirName, newFileName);
 | |
| 
 | |
|       // Check if target file already exists
 | |
|       if (await plugins.smartfile.fs.fileExists(newPath)) {
 | |
|         return {
 | |
|           oldPath: filePath,
 | |
|           newPath,
 | |
|           migrated: false,
 | |
|           error: `Target file already exists: ${newPath}`,
 | |
|         };
 | |
|       }
 | |
| 
 | |
|       if (!this.options.dryRun) {
 | |
|         // Check if we're in a git repository
 | |
|         const isGitRepo = await this.isGitRepository(this.options.baseDir);
 | |
| 
 | |
|         if (isGitRepo) {
 | |
|           // Use git mv to preserve history
 | |
|           const smartshell = new plugins.smartshell.Smartshell({
 | |
|             executor: 'bash',
 | |
|             pathDirectories: [],
 | |
|           });
 | |
|           const gitCommand = `cd "${this.options.baseDir}" && git mv "${filePath}" "${newPath}"`;
 | |
|           const result = await smartshell.exec(gitCommand);
 | |
| 
 | |
|           if (result.exitCode !== 0) {
 | |
|             throw new Error(`git mv failed: ${result.stderr}`);
 | |
|           }
 | |
|         } else {
 | |
|           // Not a git repository - cannot migrate without git
 | |
|           throw new Error('Migration requires a git repository. We have git!');
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return {
 | |
|         oldPath: filePath,
 | |
|         newPath,
 | |
|         migrated: true,
 | |
|       };
 | |
|     } catch (error) {
 | |
|       return {
 | |
|         oldPath: filePath,
 | |
|         newPath: filePath,
 | |
|         migrated: false,
 | |
|         error: error.message,
 | |
|       };
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Check if a directory is a git repository
 | |
|    */
 | |
|   private async isGitRepository(dir: string): Promise<boolean> {
 | |
|     try {
 | |
|       const gitDir = plugins.path.join(dir, '.git');
 | |
|       return await plugins.smartfile.fs.isDirectory(gitDir);
 | |
|     } catch {
 | |
|       return false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Run the migration
 | |
|    */
 | |
|   async run(): Promise<MigrationSummary> {
 | |
|     const legacyFiles = await this.findLegacyFiles();
 | |
| 
 | |
|     console.log('');
 | |
|     console.log(cs('='.repeat(60), 'blue'));
 | |
|     console.log(cs('Test File Migration Tool', 'blue'));
 | |
|     console.log(cs('='.repeat(60), 'blue'));
 | |
|     console.log('');
 | |
| 
 | |
|     if (this.options.dryRun) {
 | |
|       console.log(cs('🔍 DRY RUN MODE - No files will be modified', 'orange'));
 | |
|       console.log('');
 | |
|     }
 | |
| 
 | |
|     console.log(`Found ${legacyFiles.length} legacy test file(s)`);
 | |
|     console.log('');
 | |
| 
 | |
|     const results: MigrationResult[] = [];
 | |
|     let migratedCount = 0;
 | |
|     let errorCount = 0;
 | |
| 
 | |
|     for (const file of legacyFiles) {
 | |
|       const result = await this.migrateFile(file);
 | |
|       results.push(result);
 | |
| 
 | |
|       if (result.migrated) {
 | |
|         migratedCount++;
 | |
|         const oldName = plugins.path.basename(result.oldPath);
 | |
|         const newName = plugins.path.basename(result.newPath);
 | |
| 
 | |
|         if (this.options.dryRun) {
 | |
|           console.log(cs(`  Would migrate:`, 'cyan'));
 | |
|         } else {
 | |
|           console.log(cs(`  ✓ Migrated:`, 'green'));
 | |
|         }
 | |
|         console.log(`    ${oldName}`);
 | |
|         console.log(cs(`    → ${newName}`, 'green'));
 | |
|         console.log('');
 | |
|       } else if (result.error) {
 | |
|         errorCount++;
 | |
|         console.log(cs(`  ✗ Failed: ${plugins.path.basename(result.oldPath)}`, 'red'));
 | |
|         console.log(cs(`    ${result.error}`, 'red'));
 | |
|         console.log('');
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     console.log(cs('='.repeat(60), 'blue'));
 | |
|     console.log(`Summary:`);
 | |
|     console.log(`  Total legacy files: ${legacyFiles.length}`);
 | |
|     console.log(`  Successfully migrated: ${migratedCount}`);
 | |
|     console.log(`  Errors: ${errorCount}`);
 | |
|     console.log(cs('='.repeat(60), 'blue'));
 | |
| 
 | |
|     if (this.options.dryRun && legacyFiles.length > 0) {
 | |
|       console.log('');
 | |
|       console.log(cs('To apply these changes, run:', 'orange'));
 | |
|       console.log(cs('  tstest migrate --write', 'orange'));
 | |
|     }
 | |
| 
 | |
|     console.log('');
 | |
| 
 | |
|     return {
 | |
|       totalLegacyFiles: legacyFiles.length,
 | |
|       migratedCount,
 | |
|       errorCount,
 | |
|       results,
 | |
|       dryRun: this.options.dryRun,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Create a migration report without performing the migration
 | |
|    */
 | |
|   async generateReport(): Promise<string> {
 | |
|     const legacyFiles = await this.findLegacyFiles();
 | |
| 
 | |
|     let report = '';
 | |
|     report += 'Test File Migration Report\n';
 | |
|     report += '='.repeat(60) + '\n';
 | |
|     report += '\n';
 | |
|     report += `Found ${legacyFiles.length} legacy test file(s)\n`;
 | |
|     report += '\n';
 | |
| 
 | |
|     for (const file of legacyFiles) {
 | |
|       const fileName = plugins.path.basename(file);
 | |
|       const newFileName = getLegacyMigrationTarget(fileName);
 | |
| 
 | |
|       if (newFileName) {
 | |
|         report += `${fileName}\n`;
 | |
|         report += `  → ${newFileName}\n`;
 | |
|         report += '\n';
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     report += '='.repeat(60) + '\n';
 | |
| 
 | |
|     return report;
 | |
|   }
 | |
| }
 |