@nxworker/workspace
Version:
Nx plugin providing generators for managing workspace files, including the move-file generator for safely moving files between projects while updating all imports
219 lines (218 loc) • 10.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
default: function() {
return _default;
},
moveFileGenerator: function() {
return moveFileGenerator;
}
});
const _devkit = require("@nx/devkit");
const _workspace = require("@nx/workspace");
const _jscodeshiftutils = require("./jscodeshift-utils");
const _treecache = require("./tree-cache");
const _cachedtreeexists = require("./cache/cached-tree-exists");
const _getprojectsourcefiles = require("./cache/get-project-source-files");
const _updateprojectsourcefilescache = require("./cache/update-project-source-files-cache");
const _updatefileexistencecache = require("./cache/update-file-existence-cache");
const _getcacheddependentprojects = require("./cache/get-cached-dependent-projects");
const _splitpatterns = require("./path-utils/split-patterns");
const _isprojectempty = require("./project-analysis/is-project-empty");
const _getdependentprojectnames = require("./project-analysis/get-dependent-project-names");
const _readcompilerpaths = require("./project-analysis/read-compiler-paths");
const _resolveandvalidate = require("./validation/resolve-and-validate");
const _executemove = require("./core-operations/execute-move");
/**
* Cache for source files per project to avoid repeated tree traversals.
* Key: project root path, Value: array of source file paths
*/ const projectSourceFilesCache = new Map();
/**
* Cache for file existence checks to avoid repeated tree.exists() calls.
* Key: file path, Value: boolean indicating if file exists
*/ const fileExistenceCache = new Map();
/**
* Cache for dependent project lookups to avoid repeated graph traversals.
* Key: project name, Value: set of dependent project names
*/ const dependencyGraphCache = new Map();
/**
* Wrapper for clearAllCaches that passes cache state
*/ function clearAllCaches() {
// Clear local caches
projectSourceFilesCache.clear();
fileExistenceCache.clear();
dependencyGraphCache.clear();
// Clear compiler paths cache from project-analysis module
(0, _readcompilerpaths.clearCompilerPathsCache)();
}
/**
* Wrapper for getProjectSourceFiles that passes cache state
*/ function getProjectSourceFiles(tree, projectRoot) {
return (0, _getprojectsourcefiles.getProjectSourceFiles)(tree, projectRoot, projectSourceFilesCache, fileExistenceCache);
}
/**
* Wrapper for updateProjectSourceFilesCache that passes cache state
*/ function updateProjectSourceFilesCache(projectRoot, oldPath, newPath) {
(0, _updateprojectsourcefilescache.updateProjectSourceFilesCache)(projectRoot, oldPath, newPath, projectSourceFilesCache);
}
/**
* Wrapper for cachedTreeExists that passes cache state
*/ function cachedTreeExists(tree, filePath) {
return (0, _cachedtreeexists.cachedTreeExists)(tree, filePath, fileExistenceCache);
}
/**
* Wrapper for updateFileExistenceCache that passes cache state
*/ function updateFileExistenceCache(filePath, exists) {
(0, _updatefileexistencecache.updateFileExistenceCache)(filePath, exists, fileExistenceCache);
}
async function moveFileGenerator(tree, options) {
// Clear all caches at the start of generator execution
clearAllCaches();
// Clear AST cache at the start of each move operation
(0, _jscodeshiftutils.clearCache)();
const projects = (0, _devkit.getProjects)(tree);
// Lazily create project graph only when needed (cross-project moves with exported files)
// This improves performance for same-project moves by ~15-20%
// The graph is created on first call to getProjectGraphAsync() and cached for subsequent calls
let projectGraph = null;
const getProjectGraphAsync = async ()=>{
projectGraph ??= await (0, _devkit.createProjectGraphAsync)();
return projectGraph;
};
// Support comma-separated file paths and glob patterns
// We need to be careful about commas inside brace expansions like {ts,js}
const patterns = (0, _splitpatterns.splitPatterns)(options.file);
if (patterns.length === 0) {
throw new Error('At least one file path or glob pattern must be provided');
}
// Expand glob patterns to actual file paths
// Separate glob patterns from direct file paths for batch processing
const globPatterns = [];
const directPaths = [];
const patternMap = new Map(); // normalized -> original for error messages
for (const pattern of patterns){
// Normalize pattern to use forward slashes (Windows compatibility)
const normalizedPattern = (0, _devkit.normalizePath)(pattern);
// Check if pattern contains glob characters
const isGlobPattern = /[*?[\]{}]/.test(normalizedPattern);
if (isGlobPattern) {
globPatterns.push(normalizedPattern);
patternMap.set(normalizedPattern, pattern);
} else {
// Direct file path
directPaths.push(normalizedPattern);
}
}
// Batch all glob patterns into a single globAsync call for better performance
const filePaths = [
...directPaths
];
if (globPatterns.length > 0) {
const matches = await (0, _devkit.globAsync)(tree, globPatterns);
// If no matches at all, we need to check individual patterns for better error messages
// Only do this in the error case to maintain performance in the success case
if (matches.length === 0 && globPatterns.length > 0) {
// Find the first pattern that matches nothing for a helpful error message
for (const globPattern of globPatterns){
const individualMatches = await (0, _devkit.globAsync)(tree, [
globPattern
]);
if (individualMatches.length === 0) {
const originalPattern = patternMap.get(globPattern) || globPattern;
throw new Error(`No files found matching glob pattern: "${originalPattern}"`);
}
}
// If we get here, all patterns individually matched something, but combined they didn't
// This shouldn't happen, but throw a generic error just in case
throw new Error(`No files found matching glob patterns: "${globPatterns.join(', ')}"`);
}
filePaths.push(...matches);
}
// Remove duplicates (in case multiple patterns match the same file)
const uniqueFilePaths = Array.from(new Set(filePaths));
if (uniqueFilePaths.length === 0) {
throw new Error('At least one file path must be provided');
}
// Validate and resolve all files upfront
const contexts = uniqueFilePaths.map((filePath)=>{
const fileOptions = {
...options,
file: filePath
};
return (0, _resolveandvalidate.resolveAndValidate)(tree, fileOptions, projects, cachedTreeExists, getProjectSourceFiles);
});
// Track unique source projects for removal check
const sourceProjectNames = new Set();
contexts.forEach((ctx)=>{
sourceProjectNames.add(ctx.sourceProjectName);
});
// Execute all moves without deleting sources yet
// Note: These must be executed sequentially, not in parallel, because:
// 1. Multiple files might be moved to the same target project
// 2. updateProjectSourceFilesCache() modifies shared cache arrays
// 3. Concurrent modifications could cause race conditions
for(let i = 0; i < contexts.length; i++){
const ctx = contexts[i];
const fileOptions = {
...options,
file: uniqueFilePaths[i]
};
await (0, _executemove.executeMove)(tree, fileOptions, projects, getProjectGraphAsync, ctx, cachedTreeExists, updateProjectSourceFilesCache, updateFileExistenceCache, getProjectSourceFiles, getCachedDependentProjects, true);
}
// Delete all source files after all moves are complete
for (const ctx of contexts){
tree.delete(ctx.normalizedSource);
// Update file existence cache
updateFileExistenceCache(ctx.normalizedSource, false);
// Invalidate tree read cache
_treecache.treeReadCache.invalidateFile(ctx.normalizedSource);
}
// Check if any source projects should be removed
if (options.removeEmptyProject) {
for (const projectName of sourceProjectNames){
const project = projects.get(projectName);
if (project && (0, _isprojectempty.isProjectEmpty)(tree, project)) {
_devkit.logger.verbose(`Project ${projectName} is empty, removing it`);
try {
await (0, _workspace.removeGenerator)(tree, {
projectName,
skipFormat: true,
forceRemove: false
});
} catch (error) {
_devkit.logger.error(`Failed to remove empty project ${projectName}: ${error}`);
}
}
}
}
// Format files once at the end
if (!options.skipFormat) {
await (0, _devkit.formatFiles)(tree);
}
// Log cache statistics for performance monitoring
const cacheStats = (0, _jscodeshiftutils.getCacheStats)();
const fileExistenceCacheSize = fileExistenceCache.size;
const projectCacheSize = projectSourceFilesCache.size;
// Note: compilerPaths cache is managed in project-analysis module
const treeStats = _treecache.treeReadCache.getStats();
const dependencyGraphCacheSize = dependencyGraphCache.size;
_devkit.logger.verbose(`AST Cache stats: ${cacheStats.astCacheSize} cached ASTs, ${cacheStats.contentCacheSize} cached files, ${cacheStats.failedParseCount} parse failures`);
_devkit.logger.verbose(`File cache stats: ${projectCacheSize} project caches, ${fileExistenceCacheSize} file existence checks`);
_devkit.logger.verbose(`Tree cache stats: ${treeStats.contentCacheSize} file reads cached, ${treeStats.childrenCacheSize} directory listings cached`);
_devkit.logger.verbose(`Dependency graph cache: ${dependencyGraphCacheSize} project dependencies cached`);
}
/**
* Wrapper for getCachedDependentProjects that passes cache state
*/ function getCachedDependentProjects(projectGraph, projectName) {
return (0, _getcacheddependentprojects.getCachedDependentProjects)(projectGraph, projectName, _getdependentprojectnames.getDependentProjectNames, dependencyGraphCache);
}
const _default = moveFileGenerator;
//# sourceMappingURL=generator.js.map