file-mover
Version:
Script to move files and update imports automatically
229 lines âĸ 10.1 kB
JavaScript
// File operations for moving files and updating imports
import { promises as fs } from "fs";
import { parse } from "@babel/parser";
import traverseModule from "@babel/traverse";
import { checkIfFileIsPartOfMove, extractImportInfo, handleMovingFileImportsUpdate, handlePackageImportsUpdate, isMonorepoPackageImport, isRelativeImport, } from "./importUtils.js";
import { getPerformance } from "./performance/moveTracker.js";
import path from "path";
import { isIndexFile } from "./pathUtils.js";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const traverse = traverseModule.default || traverseModule;
// OPTIMIZATION: Cache file contents to avoid redundant reads
const fileContentCache = new Map();
globalThis.fileContentCache = fileContentCache;
export async function movePhysicalFile(oldPath, newPath) {
const perf = getPerformance(globalThis.appState.verbose);
const moveTimer = perf.startTimer(`Physical file move: ${oldPath} â ${newPath}`);
console.log(`đĻ Moving file: ${oldPath} â ${newPath}`);
await fs.rename(oldPath, newPath);
const moveTime = moveTimer.end();
perf.addFileOpTime("move", moveTime);
// Update cache with new path
const content = fileContentCache.get(oldPath);
if (content) {
fileContentCache.set(newPath, content);
fileContentCache.delete(oldPath);
}
}
const readFileWithValidation = async (filePath) => {
let currentFilePath = filePath;
try {
if (checkIfFileIsPartOfMove(currentFilePath)) {
const latestPath = globalThis.appState.fileMoveMap.get(currentFilePath);
if (latestPath) {
currentFilePath = latestPath;
}
}
await fs.access(currentFilePath);
}
catch (accessError) {
console.error(`â File not found: ${filePath}`);
console.error(` This might indicate a race condition or path resolution issue.`);
throw accessError;
}
// OPTIMIZATION: Check cache first
const cachedContent = fileContentCache.get(currentFilePath);
// Track cache performance
const perf = getPerformance(globalThis.appState.verbose);
perf.trackCacheLookup();
if (cachedContent) {
perf.trackCacheHit("file");
return cachedContent;
}
const content = await fs.readFile(currentFilePath, "utf8");
fileContentCache.set(currentFilePath, content);
return content;
};
export async function updateImportsInFile({ currentFilePath, imports, }) {
try {
let fileContent = await readFileWithValidation(currentFilePath);
let hasChanges = false;
for (const importInfo of imports) {
const currentImportPath = importInfo.importPath;
const { updated, updatedFileContent, updatedImportPath } = handlePackageImportsUpdate({
currentImportPath,
currentFilePath,
newPath: importInfo.matchedUpdateToFilePath,
fileContent,
});
hasChanges = hasChanges || updated;
if (updated) {
fileContent = updatedFileContent;
}
if (globalThis.appState.verbose && hasChanges) {
console.log(` đ ${currentFilePath}: ${currentImportPath} â ${updatedImportPath}`);
}
}
if (hasChanges) {
const perf = getPerformance();
const writeTimer = perf.startTimer(`File write: ${currentFilePath}`);
await fs.writeFile(currentFilePath, fileContent, "utf8");
const writeTime = writeTimer.end();
perf.addFileOpTime("write", writeTime);
// Update cache with new content
fileContentCache.set(currentFilePath, fileContent);
return true;
}
return false;
}
catch (error) {
console.error(`â Error updating ${currentFilePath}:`, error instanceof Error ? error.message : String(error));
return false;
}
}
function handleWithinModuleImports(pathNode, content, relativeImports) {
const importPath = pathNode.node.source?.value;
if (typeof importPath === "string" && (isRelativeImport(importPath) || isMonorepoPackageImport(importPath))) {
relativeImports.push(extractImportInfo({ pathNode, content, importPath, matchedUpdateToFilePath: "" }));
}
}
//TODO:
// 1. Should be a different algorithm for it to update to other files
// 1.1 while traversing other files, I should fine if there's a file that's relative to the current file and find the import path if
// 1.1.1 if it's intra module, I should update with relative path
// 1.1.2 if it's inter module, I should update with ms import path
export async function updateImportsInMovedFile(oldPath, newPath) {
try {
console.log(`đ Updating imports inside moved file: ${newPath}`);
// this should always be the latest path
const content = await fs.readFile(newPath, "utf8");
let updatedContent = content;
let hasChanges = false;
let needsManualResolution = false;
let ast;
try {
ast = parse(content, {
sourceType: "unambiguous",
plugins: ["typescript", "jsx", "decorators-legacy", "classProperties", "dynamicImport"],
});
}
catch (e) {
if (globalThis.appState.verbose) {
console.warn(`â ī¸ Could not parse moved file ${newPath}: ${e instanceof Error ? e.message : String(e)}`);
}
return;
}
const relativeImports = [];
// Should probably update the files whiles we are traversing the set
traverse(ast, {
ImportDeclaration: (pathNode) => {
handleWithinModuleImports(pathNode, content, relativeImports);
},
CallExpression: (pathNode) => {
const callee = pathNode.node.callee;
if ((callee.type === "Identifier" && callee.name === "require") || callee.type === "Import") {
const arg0 = pathNode.node.arguments[0];
if (arg0 && arg0.type === "StringLiteral") {
const importPath = arg0.value;
if (typeof importPath === "string" && isRelativeImport(importPath)) {
relativeImports.push(extractImportInfo({
pathNode,
content,
importPath,
matchedUpdateToFilePath: "",
}));
}
}
}
},
ExportAllDeclaration: (pathNode) => {
const source = pathNode.node.source?.value;
if (typeof source === "string" && isRelativeImport(source)) {
relativeImports.push(extractImportInfo({ pathNode, content, importPath: source, matchedUpdateToFilePath: "" }));
}
},
});
if (globalThis.appState.verbose) {
console.log(`Found ${relativeImports.length} relative imports to update`);
}
for (const importInfo of relativeImports) {
//TODO:
// 1. Find all potential import path pattern and replace them all with the relative + monorepo import path
const { updated, updatedFileContent, updatedImportPath } = handleMovingFileImportsUpdate({
importPath: importInfo.importPath,
originalMovedFilePath: oldPath,
newMovedFilePath: newPath,
fileContent: updatedContent,
});
if (updated) {
updatedContent = updatedFileContent;
hasChanges = true;
if (globalThis.appState.verbose) {
console.log(` đ Updated import: ${importInfo.importPath} â ${updatedImportPath}`);
}
}
}
if (hasChanges) {
await fs.writeFile(newPath, updatedContent, "utf8");
console.log(` â
Updated ${relativeImports.length} imports in moved file`);
if (needsManualResolution) {
console.log(` â ī¸ Manual resolution needed for imports`);
}
}
else if (globalThis.appState.verbose) {
console.log(` âšī¸ No import updates needed in moved file`);
}
}
catch (error) {
console.error(`â Error updating imports in moved file ${newPath}:`, error instanceof Error ? error.message : String(error));
}
}
/**
* Recursively get all files in a directory and their corresponding target paths
*/
export const getDirectoryMoves = async (sourceDir, targetDir) => {
const moves = [];
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
for (const entry of entries) {
const sourcePath = path.join(sourceDir, entry.name);
const targetPath = path.join(targetDir, entry.name);
if (entry.isDirectory()) {
// Recursively get moves for subdirectories
const subMoves = await getDirectoryMoves(sourcePath, targetPath);
moves.push(...subMoves);
}
else {
// Add file move
moves.push([sourcePath, targetPath]);
moves.push(...addIndexFileMoves(sourcePath, targetPath));
}
}
return moves;
};
/**
* Add index file directory moves if the file is an index file
*/
export const addIndexFileMoves = (fromPath, toPath) => {
const moves = [];
// If this is an index file, also add the directory path without /index
if (isIndexFile(fromPath)) {
const dirPath = path.dirname(fromPath);
const dirTargetPath = path.dirname(toPath);
if (globalThis.appState.verbose) {
console.log(`đ Found index file: ${fromPath}, also adding directory path: ${dirPath}`);
}
moves.push([dirPath, dirTargetPath]);
}
return moves;
};
//# sourceMappingURL=fileOps.js.map