UNPKG

image-asset-manager

Version:

A comprehensive image asset management tool for frontend projects

548 lines 21.8 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.ImageAnalyzerImpl = void 0; const path = __importStar(require("path")); const types_1 = require("../types"); const CodeScanner_1 = require("./CodeScanner"); const MetadataExtractor_1 = require("./MetadataExtractor"); class ImageAnalyzerImpl { constructor() { this.codeScanner = new CodeScanner_1.CodeScanner(); this.metadataExtractor = new MetadataExtractor_1.MetadataExtractor(); } /** * Analyze usage of image files in the project with enhanced algorithms */ async analyzeUsage(projectPath, imageFiles) { try { // Scan the project for code references const scanOptions = CodeScanner_1.CodeScanner.getDefaultOptions(); const scanResult = await this.codeScanner.scanDirectory(projectPath, scanOptions); // Filter references to only include valid image files const validReferences = await this.codeScanner.filterValidReferences(scanResult.references, imageFiles, projectPath); // Build enhanced usage map with detailed analysis const usageMap = this.buildEnhancedUsageMap(imageFiles, validReferences, projectPath); // Identify unused files with advanced detection const unusedFiles = this.identifyUnusedFilesAdvanced(imageFiles, usageMap, projectPath); return { usedFiles: usageMap, unusedFiles, totalReferences: validReferences.length, }; } catch (error) { throw new types_1.ImageAssetError(types_1.ErrorCode.FILE_NOT_FOUND, `Failed to analyze image usage in project: ${projectPath}`, error, true); } } /** * Build an enhanced map of image files to their usage information with detailed analysis */ buildEnhancedUsageMap(imageFiles, references, projectPath) { const usageMap = new Map(); // Initialize usage map with all image files for (const imageFile of imageFiles) { usageMap.set(imageFile.id, { file: imageFile, references: [], usageCount: 0, }); } // Process each reference and match it to image files for (const reference of references) { const matchingFile = this.findMatchingImageFile(reference, imageFiles, projectPath); if (matchingFile) { const usageInfo = usageMap.get(matchingFile.id); if (usageInfo) { usageInfo.references.push(reference); usageInfo.usageCount++; } } } return usageMap; } /** * Identify unused files with advanced detection algorithms * This method considers various factors to determine if a file is truly unused */ identifyUnusedFilesAdvanced(imageFiles, usageMap, projectPath) { const unusedFiles = []; for (const imageFile of imageFiles) { const usageInfo = usageMap.get(imageFile.id); // If the file has no references, it's considered unused if (!usageInfo || usageInfo.usageCount === 0) { unusedFiles.push(imageFile); } } return unusedFiles; } /** * Build a map of image files to their usage information */ buildUsageMap(imageFiles, references, projectPath) { const usageMap = new Map(); // Initialize usage map with all image files for (const imageFile of imageFiles) { usageMap.set(imageFile.id, { file: imageFile, references: [], usageCount: 0, }); } // Process each reference and match it to image files for (const reference of references) { const matchingFile = this.findMatchingImageFile(reference, imageFiles, projectPath); if (matchingFile) { const usageInfo = usageMap.get(matchingFile.id); if (usageInfo) { usageInfo.references.push(reference); usageInfo.usageCount++; } } } return usageMap; } /** * Find the image file that matches a reference */ findMatchingImageFile(reference, imageFiles, projectPath) { // Extract the image path from the reference const imagePath = this.extractImagePathFromReference(reference); if (!imagePath) return null; // Resolve the path relative to the project const resolvedPath = this.codeScanner .resolveImagePath(imagePath, reference.filePath, projectPath) .replace(/\\/g, "/"); // Find matching image file return (imageFiles.find((file) => file.relativePath.replace(/\\/g, "/") === resolvedPath) || null); } /** * Extract the actual image path from a reference context */ extractImagePathFromReference(reference) { const context = reference.context; // Common patterns to extract image paths const patterns = [ // import/require patterns /['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]/i, // url() patterns /url\s*\(\s*['"`]?([^'"`\)]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]?\s*\)/i, // src attribute patterns /(?:src|href)\s*=\s*['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]/i, ]; for (const pattern of patterns) { const match = pattern.exec(context); if (match) { return match[1]; } } return null; } /** * Identify unused image files */ identifyUnusedFiles(imageFiles, usageMap) { const unusedFiles = []; for (const imageFile of imageFiles) { const usageInfo = usageMap.get(imageFile.id); if (!usageInfo || usageInfo.usageCount === 0) { unusedFiles.push(imageFile); } } return unusedFiles; } /** * Generate safe deletion suggestions for unused files */ async generateSafeDeletionSuggestions(unusedFiles, projectPath) { const suggestions = []; for (const file of unusedFiles) { const suggestion = await this.analyzeSafeDeletion(file, projectPath); suggestions.push(suggestion); } return suggestions; } /** * Analyze if a file is safe to delete */ async analyzeSafeDeletion(file, projectPath) { const recommendations = []; let riskLevel = "low"; let safeToDelete = true; let reason = "File appears to be unused in the codebase"; // Check file age - newer files might be work in progress const daysSinceCreation = Math.floor((Date.now() - file.createdAt.getTime()) / (1000 * 60 * 60 * 24)); const daysSinceModification = Math.floor((Date.now() - file.modifiedAt.getTime()) / (1000 * 60 * 60 * 24)); if (daysSinceCreation < 7) { riskLevel = "medium"; recommendations.push("File was created recently - might be work in progress"); } if (daysSinceModification < 3) { riskLevel = "high"; safeToDelete = false; reason = "File was modified very recently - likely still being worked on"; recommendations.push("Wait a few days before considering deletion"); } // Check file size - very large files might be important if (file.size > 1024 * 1024) { // > 1MB riskLevel = riskLevel === "high" ? "high" : "medium"; recommendations.push("Large file - verify it's not used in documentation or external references"); } // Check file location - certain directories might indicate importance const pathSegments = file.relativePath.toLowerCase().split("/"); const criticalPaths = ["public", "static", "assets", "resources"]; const hasCriticalPath = pathSegments.some((segment) => criticalPaths.some((critical) => segment.includes(critical))); if (hasCriticalPath) { riskLevel = riskLevel === "high" ? "high" : "medium"; recommendations.push("Located in a public/static directory - might be referenced externally"); } // Check for common naming patterns that might indicate importance const fileName = file.name.toLowerCase(); const importantPatterns = [ "logo", "favicon", "icon", "brand", "header", "footer", ]; const hasImportantPattern = importantPatterns.some((pattern) => fileName.includes(pattern)); if (hasImportantPattern) { riskLevel = "medium"; recommendations.push("Filename suggests it might be important for branding or UI"); } // Check if it's part of a sequence or set const hasNumberInName = /\d+/.test(fileName); if (hasNumberInName) { recommendations.push("Might be part of a numbered sequence - check related files"); } // Add general recommendations if (safeToDelete) { recommendations.push("Move to a backup folder first before permanent deletion"); recommendations.push("Check if file is referenced in documentation or README files"); if (riskLevel === "low") { recommendations.push("Safe to delete after backup"); } else { recommendations.push("Review carefully before deletion"); } } return { file, safeToDelete, reason, riskLevel, recommendations, }; } /** * Get usage statistics for the project */ getUsageStatistics(usageAnalysis) { const usedFiles = Array.from(usageAnalysis.usedFiles.values()).filter((info) => info.usageCount > 0).length; const unusedFiles = usageAnalysis.unusedFiles.length; const totalFiles = usedFiles + unusedFiles; const usedFileInfos = Array.from(usageAnalysis.usedFiles.values()).filter((info) => info.usageCount > 0); const averageReferencesPerFile = usedFiles > 0 ? usageAnalysis.totalReferences / usedFiles : 0; const mostReferencedFiles = usedFileInfos .sort((a, b) => b.usageCount - a.usageCount) .slice(0, 10) .map((info) => ({ file: info.file, count: info.usageCount })); return { totalFiles, usedFiles, unusedFiles, totalReferences: usageAnalysis.totalReferences, averageReferencesPerFile, mostReferencedFiles, }; } /** * Categorize images based on their content and metadata */ async categorizeImages(files) { const categorizedFiles = [...files]; for (const file of categorizedFiles) { try { const metadata = await this.metadataExtractor.extractMetadata(file); const enhancedCategory = this.metadataExtractor.categorizeImageWithMetadata(file, metadata); // Update category if we got a more specific one if (enhancedCategory !== "general" && file.category === "general") { file.category = enhancedCategory; } } catch (error) { // Keep original category if metadata extraction fails console.warn(`Failed to enhance category for ${file.path}:`, error); } } return categorizedFiles; } /** * Generate metadata for an image file */ async generateMetadata(file) { try { return await this.metadataExtractor.extractMetadata(file); } catch (error) { throw new types_1.ImageAssetError(types_1.ErrorCode.INVALID_IMAGE_FORMAT, `Failed to generate metadata for file: ${file.path}`, error, true); } } /** * Find potentially related files based on naming patterns */ findRelatedFiles(targetFile, allFiles) { const relatedFiles = []; const targetBaseName = targetFile.name.toLowerCase(); const targetDir = path.dirname(targetFile.relativePath); for (const file of allFiles) { if (file.id === targetFile.id) continue; const fileName = file.name.toLowerCase(); const fileDir = path.dirname(file.relativePath); // Same directory const sameDirectory = fileDir === targetDir; // Similar names (common prefixes/suffixes) const hasCommonPrefix = this.hasCommonPrefix(targetBaseName, fileName, 3); const hasCommonSuffix = this.hasCommonSuffix(targetBaseName, fileName, 3); // Numbered sequence const isNumberedSequence = this.isNumberedSequence(targetBaseName, fileName); if (sameDirectory && (hasCommonPrefix || hasCommonSuffix || isNumberedSequence)) { relatedFiles.push(file); } } return relatedFiles; } /** * Check if two strings have a common prefix of minimum length */ hasCommonPrefix(str1, str2, minLength) { let commonLength = 0; const maxLength = Math.min(str1.length, str2.length); for (let i = 0; i < maxLength; i++) { if (str1[i] === str2[i]) { commonLength++; } else { break; } } return commonLength >= minLength; } /** * Check if two strings have a common suffix of minimum length */ hasCommonSuffix(str1, str2, minLength) { let commonLength = 0; const maxLength = Math.min(str1.length, str2.length); for (let i = 1; i <= maxLength; i++) { if (str1[str1.length - i] === str2[str2.length - i]) { commonLength++; } else { break; } } return commonLength >= minLength; } /** * Check if two filenames are part of a numbered sequence */ isNumberedSequence(name1, name2) { // Remove numbers and compare base names const base1 = name1.replace(/\d+/g, ""); const base2 = name2.replace(/\d+/g, ""); // If base names are similar and both contain numbers const hasNumbers1 = /\d+/.test(name1); const hasNumbers2 = /\d+/.test(name2); return hasNumbers1 && hasNumbers2 && base1 === base2; } /** * Get detailed usage report for a specific file */ getFileUsageReport(fileId, usageAnalysis) { const usageInfo = usageAnalysis.usedFiles.get(fileId); if (!usageInfo) { return { file: null, isUsed: false, usageCount: 0, references: [], referencesByType: {}, referencesByFile: {}, }; } // Group references by import type const referencesByType = {}; const referencesByFile = {}; for (const ref of usageInfo.references) { referencesByType[ref.importType] = (referencesByType[ref.importType] || 0) + 1; referencesByFile[ref.filePath] = (referencesByFile[ref.filePath] || 0) + 1; } return { file: usageInfo.file, isUsed: usageInfo.usageCount > 0, usageCount: usageInfo.usageCount, references: usageInfo.references, referencesByType, referencesByFile, }; } /** * Get files that are likely candidates for cleanup */ getCleanupCandidates(usageAnalysis, options = {}) { const { includeUnused = true, includeLowUsage = false, maxUsageCount = 1, excludePatterns = [], } = options; const unused = []; const lowUsage = []; // Filter unused files if (includeUnused) { for (const file of usageAnalysis.unusedFiles) { if (!this.shouldExcludeFromCleanup(file, excludePatterns)) { unused.push(file); } } } // Filter low usage files if (includeLowUsage) { for (const [, usageInfo] of usageAnalysis.usedFiles) { if (usageInfo.usageCount > 0 && usageInfo.usageCount <= maxUsageCount && !this.shouldExcludeFromCleanup(usageInfo.file, excludePatterns)) { lowUsage.push({ file: usageInfo.file, usageCount: usageInfo.usageCount, }); } } } return { unused, lowUsage, suggestions: [], // Will be populated by generateSafeDeletionSuggestions }; } /** * Check if a file should be excluded from cleanup suggestions */ shouldExcludeFromCleanup(file, excludePatterns) { const filePath = file.relativePath.toLowerCase(); const fileName = file.name.toLowerCase(); return excludePatterns.some((pattern) => { const lowerPattern = pattern.toLowerCase(); return filePath.includes(lowerPattern) || fileName.includes(lowerPattern); }); } /** * Analyze usage patterns across the project */ analyzeUsagePatterns(usageAnalysis) { const usedFiles = Array.from(usageAnalysis.usedFiles.values()).filter((info) => info.usageCount > 0); // Most and least used files const sortedByUsage = usedFiles.sort((a, b) => b.usageCount - a.usageCount); const mostUsedFiles = sortedByUsage .slice(0, 10) .map((info) => ({ file: info.file, count: info.usageCount })); const leastUsedFiles = sortedByUsage .slice(-10) .reverse() .map((info) => ({ file: info.file, count: info.usageCount })); // Import type distribution const importTypeDistribution = {}; for (const usageInfo of usedFiles) { for (const ref of usageInfo.references) { importTypeDistribution[ref.importType] = (importTypeDistribution[ref.importType] || 0) + 1; } } // File type usage analysis const fileTypeUsage = {}; for (const usageInfo of usageAnalysis.usedFiles.values()) { const ext = usageInfo.file.extension; if (!fileTypeUsage[ext]) { fileTypeUsage[ext] = { used: 0, unused: 0 }; } if (usageInfo.usageCount > 0) { fileTypeUsage[ext].used++; } else { fileTypeUsage[ext].unused++; } } for (const file of usageAnalysis.unusedFiles) { const ext = file.extension; if (!fileTypeUsage[ext]) { fileTypeUsage[ext] = { used: 0, unused: 0 }; } fileTypeUsage[ext].unused++; } // Directory usage analysis const directoryUsage = {}; for (const usageInfo of usageAnalysis.usedFiles.values()) { const dir = path.dirname(usageInfo.file.relativePath); if (!directoryUsage[dir]) { directoryUsage[dir] = { used: 0, unused: 0 }; } if (usageInfo.usageCount > 0) { directoryUsage[dir].used++; } else { directoryUsage[dir].unused++; } } for (const file of usageAnalysis.unusedFiles) { const dir = path.dirname(file.relativePath); if (!directoryUsage[dir]) { directoryUsage[dir] = { used: 0, unused: 0 }; } directoryUsage[dir].unused++; } return { mostUsedFiles, leastUsedFiles, importTypeDistribution, fileTypeUsage, directoryUsage, }; } } exports.ImageAnalyzerImpl = ImageAnalyzerImpl; //# sourceMappingURL=ImageAnalyzer.js.map