arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
284 lines • 10.9 kB
JavaScript
/**
* 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