image-asset-manager
Version:
A comprehensive image asset management tool for frontend projects
548 lines • 21.8 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.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