UNPKG

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
"use strict"; 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