UNPKG

file-mover

Version:

Script to move files and update imports automatically

308 lines 14.6 kB
import { normalizePath, removeExtension, getMsImportPath, resolveImportPath, getModuleType, handleMonoRepoImportPathToAbsolutePath, isIndexFile, } from "./pathUtils.js"; import { parse } from "@babel/parser"; import traverseModule from "@babel/traverse"; import path from "path"; import { getPerformance } from "./performance/moveTracker.js"; import { handleAstTraverse } from "./astTraversal.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const traverse = traverseModule.default || traverseModule; export const isRelativeImport = (importPath) => typeof importPath === "string" && importPath.startsWith("."); // Helper function to check if import path is a monorepo package import export const isMonorepoPackageImport = (importPath) => typeof importPath === "string" && importPath.startsWith("@ms/"); export const getRelativeImportPath = (fromFile, toFile) => { let relativePath = normalizePath(path.relative(path.dirname(fromFile), path.dirname(toFile))); if (!relativePath) { relativePath = "."; } const ext = path.extname(toFile); let fileName = path.basename(toFile); // Keep extension such as .png so that it'd remain valid import path if (ext === ".ts" || ext === ".tsx") { fileName = path.basename(toFile, ext); } if (relativePath === ".") { return `./${fileName}`; } else { return `${relativePath}/${fileName}`; } }; export const checkIfFileIsPartOfMove = (filePath) => globalThis.appState.fileMoveMap.has(filePath); // Helper function to extract imports from AST node export const extractImportInfo = ({ pathNode, content, importPath, matchedUpdateToFilePath, }) => { return { line: pathNode.node.loc?.start.line || 0, originalLine: content.split("\n")[pathNode.node.loc?.start?.line ? pathNode.node.loc.start.line - 1 : 0]?.trim() || "", importPath, matchedText: pathNode.toString(), matchedUpdateToFilePath, }; }; export const generateImportPathVariations = (targetPath, config) => { const normalized = path.resolve(targetPath); const paths = new Set(); // Check if this is an index file const isIndexFileResult = isIndexFile(normalized); // Helper function to add path variations (with and without extension) const addPathVariations = (basePath) => { const withoutExt = removeExtension(basePath); paths.add(normalizePath(basePath)); paths.add(normalizePath(withoutExt)); // Add directory variations if this is an index file if (isIndexFileResult) { const dirPath = path.dirname(basePath); const dirPathWithoutExt = removeExtension(dirPath); paths.add(normalizePath(dirPath)); paths.add(normalizePath(dirPathWithoutExt)); } }; // Add variations for absolute path addPathVariations(normalized); // Handle MS import path const msImportPath = getMsImportPath(normalized); if (msImportPath) { paths.add(msImportPath); // Add directory-based MS import path variations if (isIndexFileResult) { const msDirPath = msImportPath.replace(/\/index$/, ""); if (msDirPath !== msImportPath) { paths.add(msDirPath); } } } // Add variations for relative to CWD path const relativeToCwd = path.relative(config.cwd, normalized); addPathVariations(relativeToCwd); return Array.from(paths); }; // Cache for parsed ASTs to avoid re-parsing the same files const astCache = new Map(); globalThis.astCache = astCache; export const findDependencyImports = (arg) => { const { content, targetImportPaths, currentFile } = arg; const imports = []; // Performance tracking const perf = getPerformance(globalThis.appState.verbose); const readTimer = perf.startTimer(`File read: ${currentFile}`); const parseTimer = perf.startTimer(`AST parse: ${currentFile}`); const matchTimer = perf.startTimer(`Import match: ${currentFile}`); // OPTIMIZATION: Check if we have a cached AST for this file let ast = astCache.get(currentFile); // Track cache performance perf.trackCacheLookup(); if (!ast) { try { ast = parse(content, { sourceType: "unambiguous", plugins: ["typescript", "jsx", "decorators-legacy", "classProperties", "dynamicImport"], }); // Cache the AST for potential reuse astCache.set(currentFile, ast); } catch (e) { if (globalThis.appState.verbose) { console.warn(`⚠️ Could not parse ${currentFile}: ${e instanceof Error ? e.message : String(e)}`); } // End timers even on error readTimer.end(); parseTimer.end(); matchTimer.end(); return imports; } } else { // Cache hit perf.trackCacheHit("ast"); } const parseTime = parseTimer.end(); traverse(ast, handleAstTraverse({ content, targetImportPaths, currentFile, imports })); const readTime = readTimer.end(); const matchTime = matchTimer.end(); // Track detailed file analysis timing perf.trackFileAnalysis(currentFile, readTime, parseTime, matchTime, imports.length); return imports; }; export const fileMoveDirection = ({ oldPath, newPath }) => { const oldModuleType = getModuleType(oldPath); const newModuleType = getModuleType(newPath); if (oldModuleType.moduleType === newModuleType.moduleType && oldModuleType.moduleName === newModuleType.moduleName) { return "self"; } else if (oldModuleType.moduleType === "packages" && newModuleType.moduleType === "apps") { console.warn(`⚠️ Could not determine file move direction for ${oldPath}${newPath}`); return "unknown"; } else { return "betweenPackages"; } }; export const updateSrcToLib = (path) => { const matchGroups = path.match(/src\/(.*)$/); if (matchGroups) { return `lib/${matchGroups[1]}`; } return path; }; export const matchesTarget = ({ currentFile, importPath, targetImportPaths, }) => { // Track import path hits for reporting const currentCount = globalThis.appState.importPathHits.get(importPath) || 0; globalThis.appState.importPathHits.set(importPath, currentCount + 1); if (targetImportPaths.has(importPath)) { const newPath = globalThis.appState.fileMoveMap.get(targetImportPaths.get(importPath) || ""); return newPath || null; } const resolvedPath = normalizePath(resolveImportPath(currentFile, importPath)); if (targetImportPaths.has(resolvedPath)) { const newPath = globalThis.appState.fileMoveMap.get(targetImportPaths.get(resolvedPath) || ""); return newPath || null; } // const resolvedPathWithoutExt = normalizePath(removeExtension(resolvedPath)); // if (targetImportPaths.has(resolvedPathWithoutExt)) { // const newPath = globalThis.appState.fileMoveMap.get(targetImportPaths.get(resolvedPathWithoutExt) || ""); // return newPath || null; // } return null; }; //TODO: We shouldn't need to match with regex. We should be able to use the info from import analysis // to directly match and update the import statement. Since pref is minimal, defer this to later. export const createImportStatementRegexPatterns = (importPath) => { const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // Static import: import ... from '...' const staticImportPattern = new RegExp(`from\\s+(['"\`])${escapeRegex(importPath)}\\1`, "g"); // Dynamic import: import('...') const dynamicImportPattern = new RegExp(`import\\(\\s*(['"\`])${escapeRegex(importPath)}\\1\\s*\\)`, "g"); // require('...') const requirePattern = new RegExp(`require\\(\\s*(['"\`])${escapeRegex(importPath)}\\1\\s*\\)`, "g"); // jest.mock('...') const jestMockPattern = new RegExp(`jest\\.mock\\(\\s*(['"\`])${escapeRegex(importPath)}\\1\\s*,`, "g"); // jest.doMock('...') const jestDoMockPattern = new RegExp(`jest\\.doMock\\(\\s*(['"\`])${escapeRegex(importPath)}\\1\\s*\\)`, "g"); // jest.requireMock('...') const jestRequireMockPattern = new RegExp(`jest\\.requireMock\\(\\s*(['"\`])${escapeRegex(importPath)}\\1\\s*\\)`, "g"); // jest.unmock('...') const jestUnmockPattern = new RegExp(`jest\\.unmock\\(\\s*(['"\`])${escapeRegex(importPath)}\\1\\s*\\)`, "g"); // jest.dontMock('...') const jestDontMockPattern = new RegExp(`jest\\.dontMock\\(\\s*(['"\`])${escapeRegex(importPath)}\\1\\s*\\)`, "g"); return { staticImportPattern, dynamicImportPattern, requirePattern, jestMockPattern, jestDoMockPattern, jestRequireMockPattern, jestUnmockPattern, jestDontMockPattern, }; }; export const setFileContentIfRegexMatches = (fileContent, regex, replacement) => { if (regex.test(fileContent)) { return fileContent.replace(regex, replacement); } return null; }; // TODO: ideally we should just match the import path and update the import statement directly // Helper function to apply all import path replacements export const applyImportPathReplacements = (fileContent, importPath, updatedImportPath) => { const { staticImportPattern, dynamicImportPattern, requirePattern, jestMockPattern, jestDoMockPattern, jestRequireMockPattern, jestUnmockPattern, jestDontMockPattern, } = createImportStatementRegexPatterns(importPath); return (setFileContentIfRegexMatches(fileContent, staticImportPattern, `from $1${updatedImportPath}$1`) ?? setFileContentIfRegexMatches(fileContent, dynamicImportPattern, `import($1${updatedImportPath}$1)`) ?? setFileContentIfRegexMatches(fileContent, requirePattern, `require($1${updatedImportPath}$1)`) ?? setFileContentIfRegexMatches(fileContent, jestMockPattern, `jest.mock($1${updatedImportPath}$1,`) ?? setFileContentIfRegexMatches(fileContent, jestDoMockPattern, `jest.doMock($1${updatedImportPath}$1)`) ?? setFileContentIfRegexMatches(fileContent, jestRequireMockPattern, `jest.requireMock($1${updatedImportPath}$1)`) ?? setFileContentIfRegexMatches(fileContent, jestUnmockPattern, `jest.unmock($1${updatedImportPath}$1)`) ?? setFileContentIfRegexMatches(fileContent, jestDontMockPattern, `jest.dontMock($1${updatedImportPath}$1)`)); }; // This I should separate this into two functions from many to one and one to many export const handlePackageImportsUpdate = ({ currentImportPath, currentFilePath, newPath, fileContent, }) => { const fileDirection = fileMoveDirection({ oldPath: currentFilePath, newPath, }); let updatedImportPath = ""; let updated = false; let newRelativePath = getRelativeImportPath(currentFilePath, newPath); // Avoid bare import if (!newRelativePath.startsWith("../") && !newRelativePath.startsWith("./")) { newRelativePath = `./${newRelativePath}`; } if (fileDirection === "self") { if (isMonorepoPackageImport(currentImportPath)) { updatedImportPath = getRelativeImportPath(currentFilePath, handleMonoRepoImportPathToAbsolutePath(currentFilePath, newPath)); } else { updatedImportPath = newRelativePath; } } else if (fileDirection === "betweenPackages") { if (isMonorepoPackageImport(newPath)) { updatedImportPath = currentImportPath; } else { updatedImportPath = getMsImportPath(newPath); } } else { // We can't import package from app. User Have to move other dependencies to the app as well. console.warn(`⚠️ Currently not supported: ${currentFilePath}${newPath}`); return { updated, updatedFileContent: fileContent, updatedImportPath }; } // AST have walked through the file tree, we should just directly update over there // instead of search again const updatedContent = applyImportPathReplacements(fileContent, currentImportPath, updatedImportPath); return { updated: !!updatedContent, updatedFileContent: updatedContent || fileContent, updatedImportPath, }; }; export const handleMovingFileImportsUpdate = ({ importPath, originalMovedFilePath, newMovedFilePath, fileContent, }) => { const targetImportFileAbsPath = path.resolve(path.dirname(originalMovedFilePath), importPath); // Check if the imported file has been moved const importFilePath = checkIfFileIsPartOfMove(targetImportFileAbsPath) ? globalThis.appState.fileMoveMap.get(targetImportFileAbsPath) || targetImportFileAbsPath : isRelativeImport(importPath) ? targetImportFileAbsPath : importPath; const fileDirection = fileMoveDirection({ oldPath: newMovedFilePath, newPath: importFilePath, }); let updatedImportPath = ""; let updated = false; let newRelativePath = getRelativeImportPath(newMovedFilePath, importFilePath); // Avoid bare import if (!newRelativePath.startsWith("../") && !newRelativePath.startsWith("./")) { newRelativePath = `./${newRelativePath}`; } if (fileDirection === "self") { if (isMonorepoPackageImport(importPath)) { updatedImportPath = getRelativeImportPath(newMovedFilePath, handleMonoRepoImportPathToAbsolutePath(newMovedFilePath, importFilePath)); } else { updatedImportPath = newRelativePath; } } else if (fileDirection === "betweenPackages") { if (isMonorepoPackageImport(importPath)) { updatedImportPath = importPath; } else { updatedImportPath = getMsImportPath(importPath); } } else { // We can't import package from app. User Have to move other dependencies to the app as well. console.warn(`⚠️ Currently not supported: ${originalMovedFilePath}${importPath}`); return { updated, updatedFileContent: fileContent, updatedImportPath }; } // AST have walked through the file tree, we should just directly update over there // instead of search again const updatedContent = applyImportPathReplacements(fileContent, importPath, updatedImportPath); return { updated: !!updatedContent, updatedFileContent: updatedContent || fileContent, updatedImportPath, }; }; //# sourceMappingURL=importUtils.js.map