structure-validation
Version:
A Node.js CLI tool for validating codebase folder and file structure using a clean declarative configuration. Part of the guardz ecosystem for comprehensive TypeScript development.
300 lines • 12.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.MoveToUpperFolderService = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const ConfigService_1 = require("./ConfigService");
const ImportUpdateService_1 = require("./ImportUpdateService");
const FileDiscoveryService_1 = require("./FileDiscoveryService");
/**
* Dedicated service for moving files to upper folder (option 1)
* This service is completely independent with no code reuse from other services
*/
class MoveToUpperFolderService {
constructor() {
this.configService = new ConfigService_1.ConfigService('structure-validation.config.json');
this.importUpdateService = new ImportUpdateService_1.ImportUpdateService();
this.fileDiscoveryService = new FileDiscoveryService_1.FileDiscoveryService();
}
/**
* Execute the move to upper folder action
* @param filePath The path of the file to move
* @param fileName The name of the file
* @param fileExt The file extension
* @param error Optional validation error
* @param suggestion Optional validation suggestion
* @returns Promise<void>
*/
async executeMoveToUpperFolder(filePath, fileName, fileExt, error, suggestion) {
// Check if file is at root level - if so, don't allow moving to upper folder
if (await this.isFileAtRootLevel(filePath)) {
console.log('❌ Cannot move file to upper folder: file is already at root level.');
return;
}
// Get the suggested folder name from suggestion or analyze the file
let targetFolderName = suggestion?.suggestedFolder;
if (!targetFolderName) {
// Analyze the file to determine the appropriate folder
const determinedFolder = await this.determineTargetFolder(filePath);
targetFolderName = determinedFolder || undefined;
}
if (!targetFolderName) {
console.log('❌ No suitable folder found for moving to upper folder.');
return;
}
// Clean and validate the folder name
const cleanFolderName = this.cleanFolderName(targetFolderName);
if (!cleanFolderName) {
console.log('❌ Invalid folder name for upper folder move.');
return;
}
// Calculate the target paths
const currentDirectory = path.dirname(filePath);
const parentDirectory = path.dirname(currentDirectory);
const targetDirectory = path.join(parentDirectory, cleanFolderName);
const targetFilePath = path.join(targetDirectory, fileName);
// Execute the move operation with import updates
await this.performFileMove(filePath, targetFilePath, fileName, cleanFolderName);
}
/**
* Determine the target folder by analyzing the file and configuration
* @param filePath The path of the file to analyze
* @returns Promise<string | null> The suggested folder name
*/
async determineTargetFolder(filePath) {
try {
const configuration = await this.configService.loadConfig();
const fileName = path.basename(filePath);
// Analyze each rule in the configuration
for (const rule of configuration.rules) {
// Skip special patterns that are not suitable for folder suggestions
if (this.isSpecialPattern(rule.folder)) {
continue;
}
// Check if the file matches this rule's patterns
if (this.fileMatchesPatterns(fileName, rule.patterns)) {
return rule.folder;
}
}
return null;
}
catch (error) {
console.error(`❌ Failed to analyze file for folder suggestion: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
/**
* Check if a folder pattern is a special pattern that should be skipped
* @param folderName The folder name to check
* @returns boolean True if it's a special pattern
*/
isSpecialPattern(folderName) {
return folderName === '{root}' ||
folderName === '**' ||
/^\{[^}]+\}$/.test(folderName);
}
/**
* Check if filename matches any of the given patterns
* @param fileName The file name to check
* @param patterns The patterns to match against
* @returns boolean True if the file matches any pattern
*/
fileMatchesPatterns(fileName, patterns) {
return patterns.some(pattern => this.fileMatchesPattern(fileName, pattern));
}
/**
* Check if filename matches a specific pattern
* @param fileName The file name to check
* @param pattern The pattern to match against
* @returns boolean True if the file matches the pattern
*/
fileMatchesPattern(fileName, pattern) {
const regexPattern = this.convertGlobToRegex(pattern);
return regexPattern.test(fileName);
}
/**
* Convert glob pattern to regex for matching
* @param pattern The glob pattern to convert
* @returns RegExp The converted regex pattern
*/
convertGlobToRegex(pattern) {
const escaped = pattern
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/\\\*/g, '.*')
.replace(/\\\?/g, '.');
return new RegExp(`^${escaped}$`);
}
/**
* Clean and validate folder name for file system compatibility
* @param folderName The folder name to clean
* @returns string | null The cleaned folder name or null if invalid
*/
cleanFolderName(folderName) {
if (!folderName || folderName.trim().length === 0) {
return null;
}
const cleaned = folderName
.replace(/[<>:"/\\|?*]/g, '') // Remove invalid file system characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.toLowerCase() // Convert to lowercase
.trim(); // Remove leading/trailing whitespace
return cleaned.length > 0 ? cleaned : null;
}
/**
* Perform the actual file move operation
* @param sourcePath The source file path
* @param targetPath The target file path
* @param fileName The file name for display
* @param folderName The folder name for display
* @returns Promise<void>
*/
async performFileMove(sourcePath, targetPath, fileName, folderName) {
try {
// Check if target file already exists
if (fs.existsSync(targetPath)) {
console.log(`❌ File already exists in ${folderName}/ directory.`);
return;
}
// Create the target directory if it doesn't exist
const targetDirectory = path.dirname(targetPath);
if (!fs.existsSync(targetDirectory)) {
fs.mkdirSync(targetDirectory, { recursive: true });
console.log(`📁 Created directory: ${folderName}/`);
}
// Move the file
fs.renameSync(sourcePath, targetPath);
console.log(`✅ Moved ${fileName} to ${folderName}/`);
// Update imports after the file move
await this.updateImportsAfterMove(sourcePath, targetPath);
// Clear file discovery cache
this.clearFileDiscoveryCache();
}
catch (error) {
console.error(`❌ Failed to move file to upper folder: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Clear file discovery cache to ensure fresh file discovery after operations
*/
clearFileDiscoveryCache() {
try {
this.fileDiscoveryService.clearCache();
}
catch (error) {
console.error(`❌ Failed to clear file discovery cache: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Update imports after a file has been moved
* @param oldPath The old file path
* @param newPath The new file path
* @returns Promise<void>
*/
async updateImportsAfterMove(oldPath, newPath) {
try {
const movedFile = {
oldPath,
newPath,
oldRelativePath: path.relative(process.cwd(), oldPath),
newRelativePath: path.relative(process.cwd(), newPath)
};
const result = await this.importUpdateService.updateImportsAfterMove([movedFile]);
if (result.updated.length > 0) {
console.log(`🔄 Updated imports in ${result.updated.length} file(s)`);
}
if (result.errors.length > 0) {
console.log(`⚠️ Import update errors: ${result.errors.length}`);
result.errors.forEach(error => console.log(` - ${error}`));
}
}
catch (error) {
console.error(`❌ Failed to update imports: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Check if file is at root level (configured root directory)
* @param filePath The absolute path of the file
* @returns boolean True if the file is at root level
*/
async isFileAtRootLevel(filePath) {
try {
// Get the directory name of the file
const fileDir = path.dirname(filePath);
// Get the configured root from the config
const config = await this.configService.loadConfig();
const configuredRoot = config.root;
if (!configuredRoot) {
// Fallback to project root if no configured root
const projectRoot = this.getProjectRoot();
return path.resolve(fileDir) === path.resolve(projectRoot);
}
// Resolve the configured root path
const resolvedConfiguredRoot = path.resolve(configuredRoot);
// Check if the file directory is the same as the configured root
return path.resolve(fileDir) === resolvedConfiguredRoot;
}
catch (error) {
console.error(`❌ Error checking if file is at root level: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
/**
* Get the project root directory
* @returns string The project root path
*/
getProjectRoot() {
let currentDir = process.cwd();
const maxDepth = 10; // Prevent infinite loops
for (let depth = 0; depth < maxDepth; depth++) {
const packageJsonPath = path.join(currentDir, 'package.json');
// Check if this directory has a package.json (indicating it's a project root)
if (fs.existsSync(packageJsonPath)) {
return currentDir;
}
// Move up one directory
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
// We've reached the filesystem root
break;
}
currentDir = parentDir;
}
// Fallback to current working directory
return process.cwd();
}
}
exports.MoveToUpperFolderService = MoveToUpperFolderService;
//# sourceMappingURL=MoveToUpperFolderService.js.map