UNPKG

file-mover

Version:

Script to move files and update imports automatically

403 lines (398 loc) • 16.7 kB
#!/usr/bin/env node import { promises as fs } from "fs"; import path from "path"; import fg from "fast-glob"; import { generateImportPathVariations } from "./importUtils.js"; import { addIndexFileMoves, getDirectoryMoves, movePhysicalFile, updateImportsInMovedFile } from "./fileOps.js"; import { removeExtension } from "./pathUtils.js"; import { MoveTracker, batchUpdateImports, analyzeImportsWithTracking } from "./performance/moveTracker.js"; globalThis.appState = { fileMoves: [], fileMoveMap: new Map(), importPathHits: new Map(), // Track import paths hit count during move verbose: process.argv.includes("--verbose"), dryRun: process.argv.includes("--dry-run"), }; const TEMP_ARGUMENTS = { cwd: path.normalize("C:/Users/jamescheung/Desktop/Work/project/power-platform-ux"), }; const INCLUDED_PACKAGE_FOLDERS = ["powerva-main", "powerva-embedded-experiences", "powerva-core"]; const INCLUDED_APPS_FOLDERS = ["powerva-microsoft-com"]; const INCLUDE_PATTERNS = [ ...INCLUDED_PACKAGE_FOLDERS.map((f) => `packages/${f}/**`), ...INCLUDED_APPS_FOLDERS.map((f) => `apps/${f}/**`), ]; // Configuration const CONFIG = { cwd: TEMP_ARGUMENTS.cwd, excludePatterns: [ "**/node_modules/**", "**/dist/**", "**/build/**", "**/lib/**", "**/.git/**", "**/*.d.ts", "**/*.json", "**/*.md", "**/*.svg", "**/*.png", "**/*.txt", "**/*.jpg", "**/*.log", "**/*.html", "**/*.gif", "**/.eslintcache", "index.management.ts", "icon-index.ts", ], includedPackageFolders: INCLUDED_PACKAGE_FOLDERS, includedAppsFolders: INCLUDED_APPS_FOLDERS, includePatterns: INCLUDE_PATTERNS, }; /** * Main function to move multiple files and update all imports * * 1. Analyze imports for files that points to all of the move files in a single batch operation * 2. Update all imports across all files in a single batch operation * 3. Process physical file moves individually * * This replaces the old per-move analysis and update approach for better performance. */ async function moveFileAndUpdateImports(moves) { const prefTracker = new MoveTracker(globalThis.appState.verbose); prefTracker.startTotalTimer(); // Expand directory moves into individual file moves const expandedMoves = []; for (const [fromPath, toPath] of moves) { try { console.log(`šŸ” Checking path: ${fromPath}`); const stats = await fs.stat(fromPath); if (stats.isDirectory()) { console.log(`šŸ“ Found directory: ${fromPath}, expanding into individual file moves`); const dirMoves = await getDirectoryMoves(fromPath, toPath); expandedMoves.push(...dirMoves); } else { expandedMoves.push([fromPath, toPath]); expandedMoves.push(...addIndexFileMoves(fromPath, toPath)); } } catch (error) { console.error(`āŒ Error checking path ${fromPath}:`, error instanceof Error ? error.message : String(error)); process.exit(1); } } globalThis.appState.fileMoves = expandedMoves.map(([fromPath, toPath]) => [ path.normalize(fromPath), path.normalize(toPath), ]); globalThis.appState.fileMoveMap = new Map([ ...globalThis.appState.fileMoves, ...globalThis.appState.fileMoves.map((entry) => [ removeExtension(entry[0]), removeExtension(entry[1]), ]), ]); console.log(`šŸš€ Starting batch move of ${expandedMoves.length} files`); // Normalize all paths in moves const normalizedMoves = expandedMoves.map(([from, to]) => [ path.resolve(CONFIG.cwd, from), path.resolve(CONFIG.cwd, to), ]); // Validate all moves first prefTracker.startValidationTimer(); console.log("šŸ” Validating all moves..."); for (const [fromPath, toPath] of normalizedMoves) { try { await validateInputs(fromPath, toPath); } catch (error) { console.error(`āŒ Validation failed for ${fromPath} → ${toPath}:`); console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } prefTracker.endValidationTimer(normalizedMoves.length); // Find all files that might contain imports (do this once for all moves) prefTracker.startFileDiscoveryTimer(); let sourceFiles = await findSourceFiles(); // filter out files that are part of the move sourceFiles = sourceFiles.filter((file) => !globalThis.appState.fileMoveMap.has(file)); prefTracker.endFileDiscoveryTimer(sourceFiles.length); console.log(`šŸ“ Found ${sourceFiles.length} source files to check`); const deadFiles = []; // OPTIMIZATION: Pre-generate import path variations for all moves to avoid repeated computation const importPathCache = new Map(); prefTracker.startPrecomputeTimer(); for (const [fromPath] of normalizedMoves) { const variations = generateImportPathVariations(fromPath, CONFIG); importPathCache.set(fromPath, variations); } const invertedImportPathCache = new Map(); for (const [key, value] of importPathCache.entries()) { value.forEach((v) => invertedImportPathCache.set(v, key)); } prefTracker.endPrecomputeTimer(); console.log("šŸ” Analyzing imports pointing to the move files..."); const importAnalysis = await analyzeImportsWithTracking(sourceFiles, invertedImportPathCache, prefTracker); console.log(`šŸ“Š Import analysis completed:`); console.log(` - ${sourceFiles.length} source files processed`); console.log(` - ${importAnalysis.length} files with imports found`); console.log("šŸ”„ Updating imports in all affected files in batch..."); const updatedFiles = await batchUpdateImports({ importAnalysis }); prefTracker.addFileOpUpdates(updatedFiles); console.log(`āœ… Batch import updates completed: ${updatedFiles} files updated`); // Process each move (physical file operations only - imports already updated in batch above) prefTracker.startFileOpsTimer(); for (let i = 0; i < normalizedMoves.length; i++) { const [fromPath, toPath] = normalizedMoves[i]; console.log(`\nšŸ“¦ Processing move ${i + 1}/${normalizedMoves.length}: ${fromPath} → ${toPath}`); const moveMetrics = { fromPath, toPath, analysisTime: 0, moveTime: 0, movedFileUpdateTime: 0, updateTime: 0, filesUpdated: 0, detailedAnalysis: { fileReadTime: 0, astParseTime: 0, importMatchingTime: 0, filesProcessed: sourceFiles.length, filesWithImports: importAnalysis.length, }, }; try { // Analyze current imports before moving prefTracker.startAnalysisTimer(i); // Get the timing data from the batch analysis const fileTimes = prefTracker.getFileAnalysisTimes(); if (fileTimes.length > 0) { moveMetrics.detailedAnalysis.fileReadTime = fileTimes.reduce((sum, file) => sum + file.readTime, 0); moveMetrics.detailedAnalysis.astParseTime = fileTimes.reduce((sum, file) => sum + file.parseTime, 0); moveMetrics.detailedAnalysis.importMatchingTime = fileTimes.reduce((sum, file) => sum + file.matchTime, 0); } console.log(`šŸ” Found ${importAnalysis.length} files importing this module`); if (globalThis.appState.dryRun) { console.log("šŸ” DRY RUN MODE - No changes will be made for this file"); console.log("Files that would be updated:"); importAnalysis.forEach(({ file, imports }) => { console.log(` ${file}: ${imports.length} import(s)`); if (globalThis.appState.verbose) { imports.forEach((imp) => { console.log(` Line ${imp.line}: ${imp.originalLine}`); }); } }); continue; } prefTracker.startMoveTimer(i); await movePhysicalFile(fromPath, toPath); moveMetrics.moveTime = prefTracker.endMoveTimer(); // TODO: If we want to let the user know files are dead, we have to return update infor of // the moved files prefTracker.startUpdateMovedFileTimer(i); await updateImportsInMovedFile(fromPath, toPath); const movedFileUpdateTime = prefTracker.endUpdateMovedFileTimer(); moveMetrics.movedFileUpdateTime = movedFileUpdateTime; // OPTIMIZATION: Batch update all imports in other files prefTracker.startUpdateTimer(i); moveMetrics.updateTime = prefTracker.endUpdateTimer(); moveMetrics.filesUpdated = updatedFiles; if (updatedFiles > 0) { console.log(`āœ… Successfully moved file and updated ${updatedFiles} files`); } else { console.log(`āš ļø This file might not have any usage. Double check if needed`); deadFiles.push(fromPath); } } catch (error) { console.error(`āŒ Error processing move ${fromPath} → ${toPath}:`, error instanceof Error ? error.message : String(error)); // Continue with other moves instead of exiting continue; } prefTracker.addMoveMetrics(moveMetrics); } prefTracker.endFileOpsTimer(); prefTracker.setTotalTime(); console.log(`\nšŸŽ‰ Batch move completed! Processed ${normalizedMoves.length} files.`); if (deadFiles.length > 0) { console.log(`āš ļø Found ${deadFiles.length} files that may or may not have any usage:`); deadFiles.forEach((file) => console.log(` - ${file}`)); } // Print performance summary await prefTracker.printSummary(); // Export import path hits summary to JSON await exportImportPathHitsToJson(); } /** * Export summary of all import paths that were hit during the move operation to JSON file */ async function exportImportPathHitsToJson() { const { importPathHits } = globalThis.appState; if (importPathHits.size === 0) { console.log("\nšŸ“Š No import paths were hit during the move operation."); return; } // Convert Map to object for JSON serialization const importPathData = Object.fromEntries(importPathHits); // Create summary object const summary = { totalUniqueImportPaths: importPathHits.size, totalHits: Array.from(importPathHits.values()).reduce((sum, count) => sum + count, 0), importPaths: importPathData, generatedAt: new Date().toISOString(), moveOperation: { totalFilesMoved: globalThis.appState.fileMoves.length, fileMoves: globalThis.appState.fileMoves, }, }; // Write to JSON file const outputPath = path.join(process.cwd(), "import-path-hits.json"); try { await fs.writeFile(outputPath, JSON.stringify(summary, null, 2), "utf8"); console.log(`\nšŸ“Š Import path hits exported to: ${outputPath}`); console.log(` - ${importPathHits.size} unique import paths`); console.log(` - ${summary.totalHits} total hits`); } catch (error) { console.error(`āŒ Failed to export import path hits: ${error instanceof Error ? error.message : String(error)}`); } } /** * Validate that the move operation is valid * @description oldPath - validate that the old path is a file or directory and if the new path file exists, throw an error * @description newPath - validate that the new path is a file or directory and if the old path file exists, throw an error. Create the new path if it doesn't exist. */ async function validateInputs(oldPath, newPath) { try { const stats = await fs.stat(oldPath); if (!stats.isFile() && !stats.isDirectory()) { throw new Error(`Source path is neither a file nor a directory: ${oldPath}`); } } catch (error) { if (error instanceof Error) { throw error; } throw new Error(`Source path does not exist: ${oldPath}`); } try { const stats = await fs.stat(newPath); if (stats.isFile()) { throw new Error(`Destination file already exists: ${newPath}`); } // Allow directory to exist - we'll be adding files to it } catch (error) { if (error && typeof error === "object" && "code" in error && error.code !== "ENOENT") { throw error; } } // Ensure destination directory exists const newDir = path.dirname(newPath); await fs.mkdir(newDir, { recursive: true }); } /** * Find all source files that might contain imports */ async function findSourceFiles() { if (globalThis.appState.verbose) { console.log("fast-glob patterns:", CONFIG.includePatterns); console.log("fast-glob options:", { cwd: CONFIG.cwd, ignore: CONFIG.excludePatterns, onlyFiles: true, dot: true, }); } // Use fast-glob with include and exclude patterns const files = await fg(CONFIG.includePatterns, { cwd: CONFIG.cwd, ignore: CONFIG.excludePatterns, onlyFiles: true, dot: true, followSymbolicLinks: false, }); // Convert relative paths to absolute and normalize return files.map((file) => path.resolve(CONFIG.cwd, file)); } /** * Display usage information */ function showUsage() { console.log(` Usage: Single file: npx tsx move-file.ts <old-path> <new-path> [options] Batch moves: npx tsx move-file.ts moves.json [options] Options: --dry-run Show what would be changed without making changes --verbose Show detailed output Examples: # Single file move npx tsx move-file.ts src/old/file.js src/new/file.js npx tsx move-file.ts src/components/Button.tsx src/ui/Button.tsx --dry-run # Batch moves from JSON file npx tsx move-file.ts file-movers.json --dry-run --verbose JSON file format: [ ["path/to/old/file1.ts", "path/to/new/file1.ts"], ["path/to/old/file2.ts", "path/to/new/file2.ts"] ] Supported file types: JavaScript: .js, .jsx, .mjs TypeScript: .ts, .tsx Vue: .vue Svelte: .svelte `); } // Export the main functionality for programmatic use export { moveFileAndUpdateImports }; // Main execution async function main() { // Check if there's a JSON file argument for batch moves try { const jsonFileArg = process.argv.find((arg) => arg.endsWith(".json")); if (jsonFileArg) { // Load moves from JSON file try { const movesContent = await fs.readFile(jsonFileArg, "utf8"); const moves = JSON.parse(movesContent); if (!Array.isArray(moves) || !moves.every((move) => Array.isArray(move) && move.length === 2 && typeof move[0] === "string" && typeof move[1] === "string")) { throw new Error("JSON file must contain an array of [fromPath, toPath] tuples"); } await moveFileAndUpdateImports(moves); } catch (error) { console.error("āŒ Error loading JSON file:", error instanceof Error ? error.message : String(error)); process.exit(1); } } else { // Original command line interface for single moves const args = process.argv.slice(2).filter((arg) => !arg.startsWith("--")); if (args.length !== 2) { showUsage(); process.exit(1); } const [oldPath, newPath] = args; await moveFileAndUpdateImports([[oldPath, newPath]]); } process.exit(0); } catch (error) { console.error("āŒ Error:", error instanceof Error ? error.message : String(error)); process.exit(1); } } // Run the main function if this file is being executed directly if (process.argv[1]?.includes("file-mover") || process.argv[0]?.includes("node")) { main().catch((error) => { console.error("āŒ Error:", error instanceof Error ? error.message : String(error)); process.exit(1); }); } //# sourceMappingURL=index.js.map