UNPKG

arela

Version:

AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.

284 lines 10.9 kB
/** * Import path update operations for slice extraction */ import path from "node:path"; import fs from "fs-extra"; export class ImportUpdater { fileMapping = new Map(); // old path -> new path originalContents = new Map(); // file path -> original content /** * Build mapping of old file locations to new locations */ buildFileMapping(plans, cwd = process.cwd()) { this.fileMapping.clear(); for (const plan of plans) { for (const movement of plan.sourceFiles) { const oldPath = path.relative(cwd, movement.source); const newPath = path.relative(cwd, movement.destination); this.fileMapping.set(oldPath, newPath); // Also add without extension for imports that omit .ts/.js const oldPathNoExt = oldPath.replace(/\.(ts|tsx|js|jsx)$/, ''); const newPathNoExt = newPath.replace(/\.(ts|tsx|js|jsx)$/, ''); this.fileMapping.set(oldPathNoExt, newPathNoExt); } } console.log(`📋 Built file mapping with ${this.fileMapping.size} entries`); } /** * Find all files that need import updates and plan them */ async planImportUpdates(plans, cwd = process.cwd()) { const updates = []; const allFiles = await this.findAllSourceFiles(cwd); console.log(`🔍 Scanning ${allFiles.length} files for import updates...`); for (const filePath of allFiles) { const fullPath = path.join(cwd, filePath); const content = await fs.readFile(fullPath, "utf-8"); this.originalContents.set(fullPath, content); const imports = this.parseImports(content, filePath); const fileUpdates = this.calculateImportUpdates(imports, filePath, cwd); if (fileUpdates.length > 0) { console.log(` 📝 ${filePath}: ${fileUpdates.length} imports to update`); } updates.push(...fileUpdates); } console.log(`📊 Total: ${updates.length} import updates needed`); return updates; } /** * Apply import updates to files */ async updateImports(updates, dryRun = false, cwd = process.cwd()) { // Group updates by file const updatesByFile = new Map(); for (const update of updates) { const fullPath = path.join(cwd, update.filePath); if (!updatesByFile.has(fullPath)) { updatesByFile.set(fullPath, []); } updatesByFile.get(fullPath).push(update); } // Apply updates to each file for (const [filePath, fileUpdates] of updatesByFile) { if (dryRun) { continue; } if (!await fs.pathExists(filePath)) { continue; } let content = await fs.readFile(filePath, "utf-8"); const lines = content.split("\n"); // Sort by line number in reverse to maintain correct indices fileUpdates.sort((a, b) => b.lineNumber - a.lineNumber); for (const update of fileUpdates) { const lineIndex = update.lineNumber - 1; if (lineIndex >= 0 && lineIndex < lines.length) { lines[lineIndex] = lines[lineIndex].replace(update.oldImport, update.newImport); } } const updatedContent = lines.join("\n"); await fs.writeFile(filePath, updatedContent); } } /** * Parse imports from file content */ parseImports(content, filePath) { const imports = []; const lines = content.split("\n"); // Determine file type const ext = path.extname(filePath); let type = "esm"; if (ext === ".ts" || ext === ".tsx") { type = "ts"; } else if (ext === ".js" || ext === ".jsx") { type = "cjs"; // Could be either, but assume CJS } else if (ext === ".py") { type = "python"; } else if (ext === ".go") { type = "go"; } // Parse based on file type if (type === "ts" || type === "esm" || type === "cjs") { imports.push(...this.parseJavaScriptImports(content, lines)); } else if (type === "python") { imports.push(...this.parsePythonImports(content, lines)); } else if (type === "go") { imports.push(...this.parseGoImports(content, lines)); } return imports; } /** * Parse JavaScript/TypeScript imports */ parseJavaScriptImports(content, lines) { const imports = []; // ESM imports: import ... from '...' const esmRegex = /import\s+(?:[\w\s,{}*]*\s+)?from\s+['"`]([^'"`]+)['"`]/g; let match; while ((match = esmRegex.exec(content)) !== null) { const specifier = match[1]; const lineNumber = content.substring(0, match.index).split("\n").length; const fullLine = lines[lineNumber - 1] || ""; imports.push({ type: "esm", specifier, isRelative: specifier.startsWith("."), lineNumber, originalLine: fullLine, }); } // CommonJS requires: require('...') const cjsRegex = /require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g; while ((match = cjsRegex.exec(content)) !== null) { const specifier = match[1]; const lineNumber = content.substring(0, match.index).split("\n").length; const fullLine = lines[lineNumber - 1] || ""; imports.push({ type: "cjs", specifier, isRelative: specifier.startsWith("."), lineNumber, originalLine: fullLine, }); } return imports; } /** * Parse Python imports */ parsePythonImports(content, lines) { const imports = []; // Python relative imports: from . import, from .. import, from .module import const relativeRegex = /from\s+(\.+[\w.]*)\s+import|import\s+(\.+[\w.]*)/g; let match; while ((match = relativeRegex.exec(content)) !== null) { const specifier = match[1] || match[2]; const lineNumber = content.substring(0, match.index).split("\n").length; const fullLine = lines[lineNumber - 1] || ""; imports.push({ type: "python", specifier, isRelative: true, lineNumber, originalLine: fullLine, }); } return imports; } /** * Parse Go imports */ parseGoImports(content, lines) { const imports = []; // Go relative imports are handled differently, just track them const importBlockRegex = /import\s*\(\s*([\s\S]*?)\s*\)|\s+(?:[\w]+\s+)?"([^"]+)"/g; let match; while ((match = importBlockRegex.exec(content)) !== null) { const specifier = match[2] || match[1]; if (specifier && specifier.includes("/")) { const lineNumber = content.substring(0, match.index).split("\n").length; const fullLine = lines[lineNumber - 1] || ""; imports.push({ type: "go", specifier, isRelative: !specifier.includes(":") && specifier.startsWith("."), lineNumber, originalLine: fullLine, }); } } return imports; } /** * Calculate import updates needed for a file */ calculateImportUpdates(imports, filePath, cwd) { const updates = []; const fileDir = path.dirname(filePath); for (const imp of imports) { if (!imp.isRelative) { // Skip absolute imports (node_modules, etc.) continue; } // Resolve the import to an absolute path let importedPath; try { importedPath = path.normalize(path.join(fileDir, imp.specifier)); } catch { continue; } // Check if the imported file has been moved const newImportPath = this.fileMapping.get(importedPath); if (!newImportPath) { continue; // File not in our moves } // Calculate new relative path const newFileDir = path.dirname(filePath); const newRelativePath = path.relative(newFileDir, newImportPath); const normalizedPath = newRelativePath.startsWith(".") ? newRelativePath : `./${newRelativePath}`; // Create the new import statement const newImport = imp.originalLine.replace(imp.specifier, normalizedPath); updates.push({ filePath, oldImport: imp.specifier, newImport: normalizedPath, lineNumber: imp.lineNumber, }); } return updates; } /** * Find all source files in the project */ async findAllSourceFiles(cwd) { const sourceFiles = []; const extensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"]; const walk = async (dir, prefix = "") => { const entries = await fs.readdir(dir); for (const entry of entries) { // Skip common non-source directories if (entry === "node_modules" || entry === ".git" || entry === ".arela" || entry === "dist" || entry === "build" || entry === "coverage") { continue; } const fullPath = path.join(dir, entry); const relativePath = prefix ? `${prefix}/${entry}` : entry; const stat = await fs.stat(fullPath); if (stat.isDirectory()) { await walk(fullPath, relativePath); } else if (extensions.some(ext => entry.endsWith(ext))) { sourceFiles.push(relativePath); } } }; await walk(cwd); return sourceFiles; } /** * Rollback import updates */ async rollback() { for (const [filePath, originalContent] of this.originalContents) { if (await fs.pathExists(filePath)) { await fs.writeFile(filePath, originalContent); } } this.originalContents.clear(); this.fileMapping.clear(); } } //# sourceMappingURL=import-updater.js.map