UNPKG

image-asset-manager

Version:

A comprehensive image asset management tool for frontend projects

314 lines 11.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.CodeScanner = void 0; const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const types_1 = require("../types"); class CodeScanner { /** * Scan a directory for source code files and extract image references */ async scanDirectory(projectPath, options) { const references = []; const scannedFiles = []; const errors = []; try { await this.scanDirectoryRecursive(projectPath, projectPath, references, scannedFiles, errors, options, 0); return { references, scannedFiles, errors, }; } catch (error) { throw new types_1.ImageAssetError(types_1.ErrorCode.FILE_NOT_FOUND, `Failed to scan directory for code references: ${projectPath}`, error, true); } } /** * Recursively scan directories for source code files */ async scanDirectoryRecursive(currentPath, basePath, references, scannedFiles, errors, options, currentDepth) { // Check depth limit if (options.maxDepth > 0 && currentDepth >= options.maxDepth) { return; } try { const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentPath, entry.name); const relativePath = path.relative(basePath, fullPath); // Check if should be excluded if (this.shouldExclude(relativePath, options.excludePatterns)) { continue; } if (entry.isDirectory() && options.recursive) { await this.scanDirectoryRecursive(fullPath, basePath, references, scannedFiles, errors, options, currentDepth + 1); } else if (entry.isFile() && this.isSourceCodeFile(fullPath)) { try { const fileReferences = await this.scanFile(fullPath, basePath); references.push(...fileReferences); scannedFiles.push(relativePath); } catch (error) { errors.push({ file: relativePath, error: error instanceof Error ? error.message : String(error), }); } } } } catch (error) { throw new types_1.ImageAssetError(types_1.ErrorCode.PERMISSION_DENIED, `Permission denied or directory not found: ${currentPath}`, error, true); } } /** * Scan a single source code file for image references */ async scanFile(filePath, basePath) { try { const content = await fs.readFile(filePath, "utf-8"); const relativePath = path.relative(basePath, filePath); return this.extractReferences(content, relativePath); } catch (error) { throw new types_1.ImageAssetError(types_1.ErrorCode.FILE_NOT_FOUND, `Failed to read file: ${filePath}`, error, true); } } /** * Extract image references from file content */ extractReferences(content, filePath) { const references = []; const lines = content.split("\n"); for (const patternConfig of CodeScanner.REFERENCE_PATTERNS) { let match; patternConfig.pattern.lastIndex = 0; // Reset regex state while ((match = patternConfig.pattern.exec(content)) !== null) { const imagePath = match[1]; const matchIndex = match.index; // Find line number const lineNumber = this.getLineNumber(content, matchIndex); // Get context (the line containing the match) const context = lines[lineNumber - 1]?.trim() || ""; references.push({ filePath, lineNumber, context, importType: patternConfig.type, }); } } return references; } /** * Get line number for a given character index in content */ getLineNumber(content, index) { const beforeMatch = content.substring(0, index); return beforeMatch.split("\n").length; } /** * Check if a file is a source code file we should scan */ isSourceCodeFile(filePath) { const ext = path.extname(filePath).toLowerCase(); return CodeScanner.SUPPORTED_EXTENSIONS.includes(ext); } /** * Check if a path should be excluded from scanning */ shouldExclude(filePath, excludePatterns) { const normalizedPath = filePath.replace(/\\/g, "/"); return excludePatterns.some((pattern) => { // Simple pattern matching - can be enhanced with glob patterns later if (pattern.includes("*")) { const regex = new RegExp(pattern.replace(/\*/g, ".*")); return regex.test(normalizedPath); } return normalizedPath.includes(pattern); }); } /** * Resolve relative image paths to absolute paths within the project */ resolveImagePath(referencePath, sourceFilePath, projectPath) { // If it's already an absolute path within the project, return as-is if (path.isAbsolute(referencePath)) { return referencePath; } // Resolve relative to the source file's directory const sourceDir = path.dirname(path.join(projectPath, sourceFilePath)); const resolvedPath = path.resolve(sourceDir, referencePath); // Return path relative to project root return path.relative(projectPath, resolvedPath); } /** * Filter references to only include existing image files */ async filterValidReferences(references, imageFiles, projectPath) { const imagePathSet = new Set(imageFiles.map((file) => file.relativePath.replace(/\\/g, "/"))); const validReferences = []; for (const ref of references) { // Extract the image path from the reference context const imagePath = this.extractImagePathFromReference(ref); if (!imagePath) continue; // Resolve the path relative to the project const resolvedPath = this.resolveImagePath(imagePath, ref.filePath, projectPath).replace(/\\/g, "/"); // Check if this image file exists in our scanned images if (imagePathSet.has(resolvedPath)) { validReferences.push(ref); } } return validReferences; } /** * Extract the actual image path from a reference context */ extractImagePathFromReference(reference) { const context = reference.context; // Try each pattern to extract the path for (const patternConfig of CodeScanner.REFERENCE_PATTERNS) { patternConfig.pattern.lastIndex = 0; const match = patternConfig.pattern.exec(context); if (match) { return match[1]; } } return null; } /** * Get default scan options */ static getDefaultOptions() { return { extensions: CodeScanner.SUPPORTED_EXTENSIONS, excludePatterns: [ "node_modules", ".git", "dist", "build", ".next", ".nuxt", "coverage", ".nyc_output", "*.min.js", "*.bundle.js", ], recursive: true, maxDepth: 0, // No limit }; } } exports.CodeScanner = CodeScanner; // Supported source code file extensions CodeScanner.SUPPORTED_EXTENSIONS = [ ".js", ".jsx", ".ts", ".tsx", ".vue", ".html", ".htm", ".css", ".scss", ".sass", ".less", ]; // Image file extensions to look for in references CodeScanner.IMAGE_EXTENSIONS = [ ".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".bmp", ]; // Regular expressions for different import/reference patterns CodeScanner.REFERENCE_PATTERNS = [ // ES6 imports: import icon from './icon.svg' { pattern: /import\s+(?:\w+\s+from\s+)?['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]/gi, type: "import", }, // CommonJS require: require('./icon.svg') { pattern: /require\s*\(\s*['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]\s*\)/gi, type: "require", }, // Dynamic imports: import('./icon.svg') { pattern: /import\s*\(\s*['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]\s*\)/gi, type: "import", }, // CSS url(): url('./icon.svg') { pattern: /url\s*\(\s*['"`]?([^'"`\)]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]?\s*\)/gi, type: "url", }, // HTML src attributes: <img src="./icon.svg"> { pattern: /(?:src|href)\s*=\s*['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]/gi, type: "inline", }, // JSX/TSX image imports in src: <img src={require('./icon.svg')} /> { pattern: /src\s*=\s*\{\s*require\s*\(\s*['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]\s*\)\s*\}/gi, type: "require", }, // JSX/TSX image imports: <img src={icon} /> where icon is imported // This is handled by tracking import statements separately // Vue template references: <img :src="require('./icon.svg')" /> { pattern: /:src\s*=\s*['"`][^'"`]*require\s*\(\s*['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]\s*\)[^'"`]*['"`]/gi, type: "require", }, // CSS @import: @import url('./icon.svg') { pattern: /@import\s+url\s*\(\s*['"`]?([^'"`\)]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]?\s*\)/gi, type: "url", }, // Background-image in CSS: background-image: url('./icon.svg') { pattern: /background(?:-image)?\s*:\s*url\s*\(\s*['"`]?([^'"`\)]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]?\s*\)/gi, type: "url", }, ]; //# sourceMappingURL=CodeScanner.js.map